@wpnuxt/core 2.2.2 → 2.3.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/dist/module.d.mts CHANGED
@@ -127,6 +127,42 @@ interface WPNuxtConfig {
127
127
  */
128
128
  revalidateSecret?: string;
129
129
  };
130
+ /**
131
+ * Auto-generation of fragments + queries for Custom Post Types.
132
+ *
133
+ * WPNuxt parses the downloaded `schema.graphql` at build time, finds every
134
+ * object type implementing `ContentNode`, and emits a base fragment plus
135
+ * `Listing`, `ByUri`, and `BySlug` queries for each discovered CPT. Built-in
136
+ * types (Post, Page, MediaItem, Revision, Comment) are excluded because
137
+ * they already have default queries.
138
+ *
139
+ * Generated files land in `.queries/` and can be fully overridden by
140
+ * dropping a file with the same name in `extend/queries/fragments/`
141
+ * (for fragments) or `extend/queries/` (for queries).
142
+ */
143
+ cpt?: {
144
+ /**
145
+ * Enable CPT auto-generation.
146
+ *
147
+ * @default true
148
+ */
149
+ enabled?: boolean;
150
+ /**
151
+ * Type names to skip in addition to the built-in exclusions.
152
+ *
153
+ * @example ['DraftPost', 'InternalNote']
154
+ */
155
+ exclude?: string[];
156
+ /**
157
+ * If set, only these type names will be auto-generated.
158
+ *
159
+ * Useful when you want fine-grained control over which CPTs get
160
+ * auto-generated output. Built-in exclusions still apply.
161
+ *
162
+ * @example ['Event', 'Artist']
163
+ */
164
+ include?: string[];
165
+ };
130
166
  }
131
167
 
132
168
  declare const _default: _nuxt_schema.NuxtModule<WPNuxtConfig, WPNuxtConfig, false>;
package/dist/module.d.ts CHANGED
@@ -127,6 +127,42 @@ interface WPNuxtConfig {
127
127
  */
128
128
  revalidateSecret?: string;
129
129
  };
130
+ /**
131
+ * Auto-generation of fragments + queries for Custom Post Types.
132
+ *
133
+ * WPNuxt parses the downloaded `schema.graphql` at build time, finds every
134
+ * object type implementing `ContentNode`, and emits a base fragment plus
135
+ * `Listing`, `ByUri`, and `BySlug` queries for each discovered CPT. Built-in
136
+ * types (Post, Page, MediaItem, Revision, Comment) are excluded because
137
+ * they already have default queries.
138
+ *
139
+ * Generated files land in `.queries/` and can be fully overridden by
140
+ * dropping a file with the same name in `extend/queries/fragments/`
141
+ * (for fragments) or `extend/queries/` (for queries).
142
+ */
143
+ cpt?: {
144
+ /**
145
+ * Enable CPT auto-generation.
146
+ *
147
+ * @default true
148
+ */
149
+ enabled?: boolean;
150
+ /**
151
+ * Type names to skip in addition to the built-in exclusions.
152
+ *
153
+ * @example ['DraftPost', 'InternalNote']
154
+ */
155
+ exclude?: string[];
156
+ /**
157
+ * If set, only these type names will be auto-generated.
158
+ *
159
+ * Useful when you want fine-grained control over which CPTs get
160
+ * auto-generated output. Built-in exclusions still apply.
161
+ *
162
+ * @example ['Event', 'Artist']
163
+ */
164
+ include?: string[];
165
+ };
130
166
  }
131
167
 
132
168
  declare const _default: _nuxt_schema.NuxtModule<WPNuxtConfig, WPNuxtConfig, false>;
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpnuxt/core",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "configKey": "wpNuxt",
5
5
  "compatibility": {
6
6
  "nuxt": ">=4.0.0"
package/dist/module.mjs CHANGED
@@ -1,14 +1,175 @@
1
1
  import { defu } from 'defu';
2
- import { promises, cpSync, existsSync, readdirSync, statSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { writeFile, rename, readFile, mkdir } from 'node:fs/promises';
2
+ import { existsSync, readFileSync, promises, cpSync, readdirSync, statSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { mkdir, writeFile, rename, readFile } from 'node:fs/promises';
4
4
  import { join, relative, dirname } from 'node:path';
5
5
  import { useLogger, createResolver, resolveFiles, defineNuxtModule, addPlugin, addServerHandler, addImports, addComponentsDir, addTemplate, addTypeTemplate, hasNuxtModule, installModule } from '@nuxt/kit';
6
6
  import { upperFirst } from 'scule';
7
7
  import { parse, GraphQLError, visit, print } from 'graphql';
8
+ import ts from 'typescript';
8
9
  import { execSync } from 'node:child_process';
9
10
  import { consola } from 'consola';
10
11
 
11
- const version = "2.2.2";
12
+ const version = "2.3.0";
13
+
14
+ const DEFAULT_CPT_EXCLUSIONS = [
15
+ "Post",
16
+ "Page",
17
+ "MediaItem",
18
+ "Revision",
19
+ "Comment",
20
+ "ActionMonitorAction"
21
+ ];
22
+ function discoverCpts(schemaPath, options = {}) {
23
+ if (!existsSync(schemaPath)) return [];
24
+ let ast;
25
+ try {
26
+ ast = parse(readFileSync(schemaPath, "utf8"), { noLocation: true });
27
+ } catch {
28
+ return [];
29
+ }
30
+ const typesByName = /* @__PURE__ */ new Map();
31
+ const enumsByName = /* @__PURE__ */ new Map();
32
+ for (const def of ast.definitions) {
33
+ if (def.kind === "ObjectTypeDefinition") typesByName.set(def.name.value, def);
34
+ else if (def.kind === "EnumTypeDefinition") enumsByName.set(def.name.value, def);
35
+ }
36
+ const rootQuery = typesByName.get("RootQuery");
37
+ if (!rootQuery?.fields) return [];
38
+ const fieldsByType = /* @__PURE__ */ new Map();
39
+ for (const field of rootQuery.fields) {
40
+ const returnType = unwrapNamedType(field.type);
41
+ if (!returnType) continue;
42
+ const hasIdType = field.arguments?.some((a) => a.name.value === "idType");
43
+ if (typesByName.has(returnType) && hasIdType) {
44
+ const idTypeArg = field.arguments?.find((a) => a.name.value === "idType");
45
+ const idTypeEnum = idTypeArg ? unwrapNamedType(idTypeArg.type) : void 0;
46
+ const entry = fieldsByType.get(returnType) ?? {};
47
+ entry.single = field.name.value;
48
+ entry.idTypeEnum = idTypeEnum;
49
+ fieldsByType.set(returnType, entry);
50
+ continue;
51
+ }
52
+ const connMatch = /^RootQueryTo(\w+)Connection$/.exec(returnType);
53
+ const hasCursorArgs = field.arguments?.some((a) => a.name.value === "first") && field.arguments?.some((a) => a.name.value === "after");
54
+ if (connMatch && hasCursorArgs) {
55
+ const typeName = connMatch[1];
56
+ const entry = fieldsByType.get(typeName) ?? {};
57
+ entry.connection = field.name.value;
58
+ fieldsByType.set(typeName, entry);
59
+ }
60
+ }
61
+ const excludeSet = /* @__PURE__ */ new Set([...DEFAULT_CPT_EXCLUSIONS, ...options.exclude ?? []]);
62
+ const includeSet = options.include?.length ? new Set(options.include) : void 0;
63
+ const result = [];
64
+ for (const [typeName, node] of typesByName) {
65
+ if (!typeImplements(node, "ContentNode")) continue;
66
+ if (excludeSet.has(typeName)) continue;
67
+ if (includeSet && !includeSet.has(typeName)) continue;
68
+ const queryFields = fieldsByType.get(typeName);
69
+ if (!queryFields?.single || !queryFields.connection) continue;
70
+ const supportedIdTypes = /* @__PURE__ */ new Set();
71
+ if (queryFields.idTypeEnum) {
72
+ const enumDef = enumsByName.get(queryFields.idTypeEnum);
73
+ enumDef?.values?.forEach((v) => supportedIdTypes.add(v.name.value));
74
+ }
75
+ result.push({
76
+ typeName,
77
+ singleField: queryFields.single,
78
+ connectionField: queryFields.connection,
79
+ idTypeEnum: queryFields.idTypeEnum,
80
+ supportedIdTypes,
81
+ interfaces: new Set((node.interfaces ?? []).map((i) => i.name.value)),
82
+ hasField: collectScalarFieldFlags(node, ["title", "slug", "uri", "date"])
83
+ });
84
+ }
85
+ return result.sort((a, b) => a.typeName.localeCompare(b.typeName));
86
+ }
87
+ function unwrapNamedType(type) {
88
+ let current = type;
89
+ while (current) {
90
+ if (current.kind === "NamedType") return current.name.value;
91
+ current = current.type;
92
+ }
93
+ return void 0;
94
+ }
95
+ function typeImplements(node, interfaceName) {
96
+ return node.interfaces?.some((i) => i.name.value === interfaceName) ?? false;
97
+ }
98
+ function collectScalarFieldFlags(node, fieldNames) {
99
+ const flags = {};
100
+ const declared = new Set((node.fields ?? []).map((f) => f.name.value));
101
+ for (const name of fieldNames) flags[name] = declared.has(name);
102
+ return flags;
103
+ }
104
+
105
+ async function writeCptArtifacts(cpt, outputPath) {
106
+ const fragmentsDir = join(outputPath, "fragments");
107
+ const fragmentPath = join(fragmentsDir, `${cpt.typeName}.fragment.gql`);
108
+ const queryPath = join(outputPath, `${upperFirst(cpt.connectionField)}.gql`);
109
+ const wroteFragment = !existsSync(fragmentPath);
110
+ const wroteQueries = !existsSync(queryPath);
111
+ if (wroteFragment) {
112
+ await mkdir(fragmentsDir, { recursive: true });
113
+ await writeFile(fragmentPath, buildCptFragment(cpt), "utf-8");
114
+ }
115
+ if (wroteQueries) {
116
+ await writeFile(queryPath, buildCptQueries(cpt), "utf-8");
117
+ }
118
+ return { cpt, wroteFragment, wroteQueries };
119
+ }
120
+ function buildCptFragment(cpt) {
121
+ const lines = [`fragment ${cpt.typeName} on ${cpt.typeName} {`];
122
+ lines.push(" ...ContentNode");
123
+ if (cpt.interfaces.has("NodeWithExcerpt")) lines.push(" ...NodeWithExcerpt");
124
+ if (cpt.interfaces.has("NodeWithContentEditor")) lines.push(" ...NodeWithContentEditor");
125
+ if (cpt.interfaces.has("NodeWithFeaturedImage")) lines.push(" ...NodeWithFeaturedImage");
126
+ if (cpt.hasField.title) lines.push(" title");
127
+ lines.push("}");
128
+ lines.push("");
129
+ return lines.join("\n");
130
+ }
131
+ function buildCptQueries(cpt) {
132
+ const { typeName, singleField, connectionField, supportedIdTypes } = cpt;
133
+ const listQueryName = upperFirst(connectionField);
134
+ const queries = [];
135
+ queries.push(
136
+ `query ${listQueryName}($first: Int = 20, $after: String) {`,
137
+ ` ${connectionField}(first: $first, after: $after) {`,
138
+ ` nodes {`,
139
+ ` ...${typeName}`,
140
+ ` }`,
141
+ ` pageInfo {`,
142
+ ` hasNextPage`,
143
+ ` hasPreviousPage`,
144
+ ` startCursor`,
145
+ ` endCursor`,
146
+ ` }`,
147
+ ` }`,
148
+ `}`,
149
+ ""
150
+ );
151
+ if (supportedIdTypes.has("URI")) {
152
+ queries.push(
153
+ `query ${typeName}ByUri($uri: ID!) {`,
154
+ ` ${singleField}(id: $uri, idType: URI) {`,
155
+ ` ...${typeName}`,
156
+ ` }`,
157
+ `}`,
158
+ ""
159
+ );
160
+ }
161
+ if (supportedIdTypes.has("SLUG")) {
162
+ queries.push(
163
+ `query ${typeName}BySlug($slug: ID!) {`,
164
+ ` ${singleField}(id: $slug, idType: SLUG) {`,
165
+ ` ...${typeName}`,
166
+ ` }`,
167
+ `}`,
168
+ ""
169
+ );
170
+ }
171
+ return queries.join("\n");
172
+ }
12
173
 
13
174
  function createModuleError(module, message) {
14
175
  return new Error(formatErrorMessage(module, message));
@@ -51,7 +212,7 @@ const initLogger = (debug) => {
51
212
  function getLogger() {
52
213
  return loggerInstance;
53
214
  }
54
- async function mergeQueries(nuxt, wpNuxtConfig, resolver) {
215
+ async function mergeQueries(nuxt, wpNuxtConfig, resolver, schemaPath) {
55
216
  const logger = getLogger();
56
217
  const baseDir = nuxt.options.srcDir || nuxt.options.rootDir;
57
218
  const { resolve } = createResolver(baseDir);
@@ -60,6 +221,20 @@ async function mergeQueries(nuxt, wpNuxtConfig, resolver) {
60
221
  const defaultQueriesPath = resolver.resolve("./runtime/queries");
61
222
  await promises.rm(queryOutputPath, { recursive: true, force: true });
62
223
  cpSync(defaultQueriesPath, queryOutputPath, { recursive: true });
224
+ const cptSpreads = [];
225
+ if (schemaPath && wpNuxtConfig.cpt?.enabled !== false) {
226
+ const cpts = discoverCpts(schemaPath, {
227
+ exclude: wpNuxtConfig.cpt?.exclude,
228
+ include: wpNuxtConfig.cpt?.include
229
+ });
230
+ for (const cpt of cpts) {
231
+ await writeCptArtifacts(cpt, queryOutputPath);
232
+ cptSpreads.push({ name: cpt.typeName, type: cpt.typeName });
233
+ }
234
+ if (cpts.length) {
235
+ logger.debug(`Auto-generated fragments + queries for CPTs: ${cpts.map((c) => c.typeName).join(", ")}`);
236
+ }
237
+ }
63
238
  const conflicts = findConflicts(userQueryPath, queryOutputPath);
64
239
  if (conflicts.length && wpNuxtConfig.queries.warnOnOverride) {
65
240
  logger.warn("The following user query files will override default queries:");
@@ -71,26 +246,29 @@ async function mergeQueries(nuxt, wpNuxtConfig, resolver) {
71
246
  logger.debug("Extending queries:", userQueryPath);
72
247
  copyGraphqlFiles(userQueryPath, queryOutputPath);
73
248
  }
74
- await addCustomFragmentsToNodeQuery(queryOutputPath, userQueryPath, logger);
249
+ await addCustomFragmentsToNodeQuery(queryOutputPath, userQueryPath, logger, cptSpreads);
75
250
  logger.debug("Merged queries folder:", queryOutputPath);
76
251
  return queryOutputPath;
77
252
  }
78
253
  const FRAGMENT_DEF_PATTERN = /fragment\s+(\w+)\s+on\s+(\w+)/;
79
- async function addCustomFragmentsToNodeQuery(queryOutputPath, userQueryPath, logger) {
254
+ async function addCustomFragmentsToNodeQuery(queryOutputPath, userQueryPath, logger, preDiscovered = []) {
255
+ const byType = /* @__PURE__ */ new Map();
256
+ for (const f of preDiscovered) byType.set(f.type, f);
80
257
  const userFragmentsDir = join(userQueryPath, "fragments");
81
- if (!existsSync(userFragmentsDir)) return;
82
- const customFragments = [];
83
- for (const file of readdirSync(userFragmentsDir)) {
84
- if (!file.endsWith(".gql") && !file.endsWith(".graphql")) continue;
85
- const content = await promises.readFile(join(userFragmentsDir, file), "utf-8");
86
- const match = content.match(FRAGMENT_DEF_PATTERN);
87
- if (!match) continue;
88
- const name = match[1];
89
- const type = match[2];
90
- if (name && type && name === type) {
91
- customFragments.push({ name, type });
258
+ if (existsSync(userFragmentsDir)) {
259
+ for (const file of readdirSync(userFragmentsDir)) {
260
+ if (!file.endsWith(".gql") && !file.endsWith(".graphql")) continue;
261
+ const content = await promises.readFile(join(userFragmentsDir, file), "utf-8");
262
+ const match = content.match(FRAGMENT_DEF_PATTERN);
263
+ if (!match) continue;
264
+ const name = match[1];
265
+ const type = match[2];
266
+ if (name && type && name === type) {
267
+ byType.set(type, { name, type });
268
+ }
92
269
  }
93
270
  }
271
+ const customFragments = [...byType.values()];
94
272
  if (customFragments.length === 0) return;
95
273
  const nodeGqlPath = join(queryOutputPath, "Node.gql");
96
274
  if (!existsSync(nodeGqlPath)) return;
@@ -204,6 +382,46 @@ function processSelections(selections, level, query, canExtract = true) {
204
382
  }
205
383
  const parseDoc = _parseDoc;
206
384
 
385
+ const { factory, SyntaxKind, NewLineKind, EmitHint, ScriptTarget } = ts;
386
+ function typeRef(name, typeArguments) {
387
+ return factory.createTypeReferenceNode(name, typeArguments);
388
+ }
389
+ function nonNullable(node) {
390
+ return typeRef("NonNullable", [node]);
391
+ }
392
+ function withImagePath(node) {
393
+ return typeRef("WithImagePath", [node]);
394
+ }
395
+ function indexedAccess(target, key) {
396
+ return factory.createIndexedAccessTypeNode(
397
+ target,
398
+ // `isSingleQuote` preserves the single-quote style the generator used
399
+ // before this module was AST-based — keeps generated .d.ts text-stable.
400
+ factory.createLiteralTypeNode(factory.createStringLiteral(key, true))
401
+ );
402
+ }
403
+ function numberIndexedAccess(target) {
404
+ return factory.createIndexedAccessTypeNode(
405
+ target,
406
+ factory.createKeywordTypeNode(SyntaxKind.NumberKeyword)
407
+ );
408
+ }
409
+ function arrayOf(node) {
410
+ return factory.createArrayTypeNode(node);
411
+ }
412
+ function unionOrSingle(nodes) {
413
+ if (nodes.length === 0) {
414
+ throw new Error("unionOrSingle: empty type list");
415
+ }
416
+ if (nodes.length === 1) return nodes[0];
417
+ return factory.createUnionTypeNode(nodes);
418
+ }
419
+ const printer = ts.createPrinter({ newLine: NewLineKind.LineFeed, removeComments: false });
420
+ const syntheticFile = ts.createSourceFile("__synthetic.ts", "", ScriptTarget.Latest);
421
+ function printType(node) {
422
+ return printer.printNode(EmitHint.Unspecified, node, syntheticFile);
423
+ }
424
+
207
425
  const SCHEMA_PATTERN = /schema\.(?:gql|graphql)$/i;
208
426
  const COMPLEXITY_THRESHOLDS = {
209
427
  /** Maximum recommended extraction depth */
@@ -257,37 +475,40 @@ async function prepareContext(ctx) {
257
475
  const fnName = (fn) => ctx.composablesPrefix + upperFirst(fn);
258
476
  const mutationFnName = (fn) => `useMutation${upperFirst(fn)}`;
259
477
  const formatNodes = (nodes) => nodes?.map((n) => `'${n}'`).join(",") ?? "";
260
- const getFragmentType = (q) => {
478
+ const buildFragmentTypeNode = (q) => {
261
479
  if (q.hasPageInfo) {
262
480
  if (q.hasInlineFields || !q.fragments?.length) {
263
481
  if (q.nodes?.length) {
264
- let typePath = `${q.name}RootQuery`;
265
- for (const node of q.nodes) {
266
- typePath = `NonNullable<${typePath}>['${node}']`;
482
+ let node = typeRef(`${q.name}RootQuery`);
483
+ for (const segment of q.nodes) {
484
+ node = indexedAccess(nonNullable(node), segment);
267
485
  }
268
- typePath = `NonNullable<${typePath}>['nodes'][number]`;
269
- return typePath;
486
+ return numberIndexedAccess(indexedAccess(nonNullable(node), "nodes"));
270
487
  }
271
- return `${q.name}RootQuery`;
488
+ return typeRef(`${q.name}RootQuery`);
272
489
  }
273
- return q.fragments.map((f) => `WithImagePath<${f}Fragment>`).join(" | ");
490
+ return unionOrSingle(q.fragments.map((f) => withImagePath(typeRef(`${f}Fragment`))));
274
491
  }
275
492
  if (q.hasInlineFields || !q.fragments?.length) {
276
493
  if (q.nodes?.length) {
277
- let typePath = `${q.name}RootQuery`;
278
- for (const node of q.nodes) {
279
- typePath = `NonNullable<${typePath}>['${node}']`;
494
+ let node = typeRef(`${q.name}RootQuery`);
495
+ for (const segment of q.nodes) {
496
+ node = indexedAccess(nonNullable(node), segment);
280
497
  }
281
498
  if (q.nodes.includes("nodes")) {
282
- typePath = `${typePath}[number]`;
499
+ node = numberIndexedAccess(node);
283
500
  }
284
- return typePath;
501
+ return node;
285
502
  }
286
- return `${q.name}RootQuery`;
503
+ return typeRef(`${q.name}RootQuery`);
504
+ }
505
+ const fragmentTypes = q.fragments.map((f) => withImagePath(typeRef(`${f}Fragment`)));
506
+ if (q.nodes?.includes("nodes")) {
507
+ return unionOrSingle(fragmentTypes.map(arrayOf));
287
508
  }
288
- const fragmentSuffix = q.nodes?.includes("nodes") ? "[]" : "";
289
- return q.fragments.map((f) => `WithImagePath<${f}Fragment>${fragmentSuffix}`).join(" | ");
509
+ return unionOrSingle(fragmentTypes);
290
510
  };
511
+ const getFragmentType = (q) => printType(buildFragmentTypeNode(q));
291
512
  const queryFnExp = (q, typed = false) => {
292
513
  const functionName = fnName(q.name);
293
514
  if (q.hasPageInfo) {
@@ -304,7 +525,7 @@ async function prepareContext(ctx) {
304
525
  const mutationFnExp = (m, typed = false) => {
305
526
  const functionName = mutationFnName(m.name);
306
527
  if (!typed) {
307
- return `export const ${functionName} = (variables, options) => useGraphqlMutation('${m.name}', variables, options)`;
528
+ return `export const ${functionName} = (variables, options) => wpMutation('${m.name}', variables, options)`;
308
529
  }
309
530
  return ` export const ${functionName}: (variables: ${m.name}MutationVariables, options?: WPMutationOptions) => Promise<WPMutationResult<${m.name}Mutation>>`;
310
531
  };
@@ -319,11 +540,13 @@ async function prepareContext(ctx) {
319
540
  if (hasConnectionQueries) {
320
541
  imports.push("useWPConnection");
321
542
  }
322
- if (mutations.length > 0) {
323
- imports.push("useGraphqlMutation");
324
- }
325
543
  if (imports.length > 0) {
326
544
  lines.push(`import { ${imports.join(", ")} } from '#imports'`);
545
+ }
546
+ if (mutations.length > 0) {
547
+ lines.push(`import { wpMutation } from '#wpnuxt-internal'`);
548
+ }
549
+ if (lines.length > 0) {
327
550
  lines.push("");
328
551
  }
329
552
  queries.forEach((f) => {
@@ -347,6 +570,7 @@ async function prepareContext(ctx) {
347
570
  typeSet.add(`${m.name}MutationVariables`);
348
571
  typeSet.add(`${m.name}Mutation`);
349
572
  });
573
+ ctx.referencedTypes = [...typeSet];
350
574
  ctx.generateDeclarations = () => {
351
575
  const declarations = [
352
576
  `import type { ${[...typeSet].join(", ")} } from '#build/graphql-operations'`,
@@ -518,7 +742,7 @@ Make sure WPGraphQL plugin is installed and activated on your WordPress site.`
518
742
  );
519
743
  }
520
744
  await checkWPGraphQLVersion(fullUrl, headers);
521
- if (options.schemaPath && !existsSync(options.schemaPath)) {
745
+ if (options.schemaPath) {
522
746
  try {
523
747
  const authFlag = options.authToken ? ` -h "Authorization=Bearer ${options.authToken}"` : "";
524
748
  execSync(`npx get-graphql-schema "${fullUrl}"${authFlag} > "${options.schemaPath}"`, {
@@ -649,6 +873,40 @@ function patchWPGraphQLSchema(schemaPath) {
649
873
  writeFileSync(schemaPath, patchSchemaText(schema));
650
874
  }
651
875
 
876
+ function validateGeneratedPaths(referencedTypes, operationsDtsPath) {
877
+ if (!existsSync(operationsDtsPath)) {
878
+ return { skipped: true, dangling: [] };
879
+ }
880
+ const source = readFileSync(operationsDtsPath, "utf8");
881
+ const sourceFile = ts.createSourceFile(
882
+ operationsDtsPath,
883
+ source,
884
+ ts.ScriptTarget.Latest,
885
+ false
886
+ );
887
+ const declared = /* @__PURE__ */ new Set();
888
+ const visit = (node) => {
889
+ if (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isClassDeclaration(node)) {
890
+ if (node.name) declared.add(node.name.text);
891
+ } else if (ts.isVariableStatement(node)) {
892
+ for (const d of node.declarationList.declarations) {
893
+ if (ts.isIdentifier(d.name)) declared.add(d.name.text);
894
+ }
895
+ } else if (ts.isModuleDeclaration(node) && node.body && ts.isModuleBlock(node.body)) {
896
+ for (const child of node.body.statements) visit(child);
897
+ }
898
+ };
899
+ for (const stmt of sourceFile.statements) visit(stmt);
900
+ const dangling = [];
901
+ const seen = /* @__PURE__ */ new Set();
902
+ for (const ref of referencedTypes) {
903
+ if (seen.has(ref)) continue;
904
+ seen.add(ref);
905
+ if (!declared.has(ref)) dangling.push(ref);
906
+ }
907
+ return { skipped: false, dangling };
908
+ }
909
+
652
910
  async function runInstall(nuxt) {
653
911
  const logger = useLogger("wpnuxt", {
654
912
  level: process.env.WPNUXT_DEBUG === "true" ? 4 : 3
@@ -872,6 +1130,11 @@ const module$1 = defineNuxtModule({
872
1130
  maxAge: 60 * 5,
873
1131
  // 5 minutes
874
1132
  swr: true
1133
+ },
1134
+ cpt: {
1135
+ enabled: true,
1136
+ exclude: [],
1137
+ include: []
875
1138
  }
876
1139
  },
877
1140
  async setup(options, nuxt) {
@@ -890,7 +1153,6 @@ const module$1 = defineNuxtModule({
890
1153
  addPlugin(resolver.resolve("./runtime/plugins/graphqlErrors"));
891
1154
  addPlugin(resolver.resolve("./runtime/plugins/sanitizeHtml"));
892
1155
  configureTrailingSlash(nuxt, logger);
893
- const mergedQueriesFolder = await mergeQueries(nuxt, wpNuxtConfig, resolver);
894
1156
  const packageRoot = resolver.resolve("..");
895
1157
  nuxt.options._layers.push({
896
1158
  cwd: packageRoot,
@@ -906,31 +1168,21 @@ const module$1 = defineNuxtModule({
906
1168
  const schemaPath = join(nuxt.options.rootDir, "schema.graphql");
907
1169
  const schemaExists = existsSync(schemaPath);
908
1170
  if (wpNuxtConfig.downloadSchema) {
909
- if (!schemaExists) {
910
- logger.debug(`Downloading schema from: ${wpNuxtConfig.wordpressUrl}${wpNuxtConfig.graphqlEndpoint}`);
1171
+ logger.debug(`Downloading schema from: ${wpNuxtConfig.wordpressUrl}${wpNuxtConfig.graphqlEndpoint}`);
1172
+ try {
911
1173
  await validateWordPressEndpoint(
912
1174
  wpNuxtConfig.wordpressUrl,
913
1175
  wpNuxtConfig.graphqlEndpoint,
914
1176
  { schemaPath, authToken: wpNuxtConfig.schemaAuthToken }
915
1177
  );
916
1178
  logger.debug("Schema downloaded successfully");
917
- } else {
918
- nuxt.hook("ready", async () => {
919
- try {
920
- await validateWordPressEndpoint(
921
- wpNuxtConfig.wordpressUrl,
922
- wpNuxtConfig.graphqlEndpoint,
923
- { authToken: wpNuxtConfig.schemaAuthToken }
924
- );
925
- logger.debug("WordPress endpoint validation passed");
926
- } catch (error) {
927
- const message = error instanceof Error ? error.message : String(error);
928
- logger.warn(`WordPress endpoint validation failed: ${message.split("\n")[0]}`);
929
- logger.warn("App will continue with existing schema.graphql file");
930
- }
931
- });
1179
+ } catch (error) {
1180
+ if (!schemaExists) throw error;
1181
+ const message = error instanceof Error ? error.message : String(error);
1182
+ logger.warn(`Schema refresh failed, using cached schema.graphql: ${message.split("\n")[0]}`);
932
1183
  }
933
1184
  }
1185
+ const mergedQueriesFolder = await mergeQueries(nuxt, wpNuxtConfig, resolver, schemaPath);
934
1186
  await registerModules(nuxt, resolver, wpNuxtConfig, mergedQueriesFolder);
935
1187
  nuxt.hook("devtools:customTabs", (tabs) => {
936
1188
  const middlewareTab = tabs.find((tab) => tab.name === "nuxt-graphql-middleware");
@@ -1031,12 +1283,14 @@ const module$1 = defineNuxtModule({
1031
1283
  nuxt.options.alias["#wpnuxt"] = resolver.resolve(nuxt.options.buildDir, "wpnuxt");
1032
1284
  nuxt.options.alias["#wpnuxt/*"] = resolver.resolve(nuxt.options.buildDir, "wpnuxt", "*");
1033
1285
  nuxt.options.alias["#wpnuxt/types"] = resolver.resolve("./types");
1286
+ nuxt.options.alias["#wpnuxt-internal"] = resolver.resolve("./runtime/internal/graphql-client");
1034
1287
  nuxt.options.alias["@wpnuxt/core/server-options"] = resolver.resolve("./server-options");
1035
1288
  nuxt.options.alias["@wpnuxt/core/client-options"] = resolver.resolve("./client-options");
1036
1289
  const nitroOpts = nuxt.options;
1037
1290
  nitroOpts.nitro = nitroOpts.nitro || {};
1038
1291
  nitroOpts.nitro.alias = nitroOpts.nitro.alias || {};
1039
1292
  nitroOpts.nitro.alias["#wpnuxt/types"] = resolver.resolve("./types");
1293
+ nitroOpts.nitro.alias["#wpnuxt-internal"] = resolver.resolve("./runtime/internal/graphql-client");
1040
1294
  nitroOpts.nitro.externals = nitroOpts.nitro.externals || {};
1041
1295
  nitroOpts.nitro.externals.inline = nitroOpts.nitro.externals.inline || [];
1042
1296
  addTemplate({
@@ -1053,6 +1307,16 @@ const module$1 = defineNuxtModule({
1053
1307
  autoimports.push(...ctx.fnImports || []);
1054
1308
  });
1055
1309
  logger.trace("Finished generating composables");
1310
+ nuxt.hook("build:before", () => {
1311
+ if (!ctx.referencedTypes?.length) return;
1312
+ const operationsDtsPath = join(nuxt.options.buildDir, "graphql-operations.d.ts");
1313
+ const result = validateGeneratedPaths(ctx.referencedTypes, operationsDtsPath);
1314
+ if (result.skipped || result.dangling.length === 0) return;
1315
+ logger.warn(
1316
+ `WPNuxt generated composables reference ${result.dangling.length} type(s) not declared in graphql-operations.d.ts. This usually means your WordPress GraphQL schema has drifted from your queries; delete schema.graphql to force a fresh download, then re-run pnpm dev:prepare.`
1317
+ );
1318
+ for (const t of result.dangling) logger.warn(` - ${t}`);
1319
+ });
1056
1320
  logger.info(`WPNuxt module loaded in ${(/* @__PURE__ */ new Date()).getTime() - startTime}ms`);
1057
1321
  },
1058
1322
  async onInstall(nuxt) {
@@ -1,5 +1,6 @@
1
1
  import { transformData, normalizeUriParam } from "../util/content.js";
2
- import { computed, ref, toValue, watch as vueWatch, useAsyncGraphqlQuery, useRuntimeConfig } from "#imports";
2
+ import { computed, ref, toValue, watch as vueWatch, useRuntimeConfig } from "#imports";
3
+ import { wpQuery } from "../internal/graphql-client.js";
3
4
  const defaultGetCachedData = (key, app, ctx) => {
4
5
  if (app.isHydrating) {
5
6
  return app.payload.data[key];
@@ -58,7 +59,7 @@ export const useWPContent = (queryName, nodes, fixImagePaths, params, options) =
58
59
  }
59
60
  }
60
61
  };
61
- const asyncResult = useAsyncGraphqlQuery(
62
+ const asyncResult = wpQuery(
62
63
  String(queryName),
63
64
  resolvedParams,
64
65
  asyncDataOptions
File without changes
@@ -0,0 +1,73 @@
1
+ import { useAsyncGraphqlQuery, useGraphqlMutation, useNuxtApp, watch, toValue } from "#imports";
2
+ export const wpQuery = ((...args) => {
3
+ const [name, variables] = args;
4
+ const nuxtApp = useNuxtApp();
5
+ const result = useAsyncGraphqlQuery(...args);
6
+ let wasPending = false;
7
+ let cycleStart = 0;
8
+ const pendingSource = result.pending;
9
+ const errorSource = result.error;
10
+ watch(
11
+ pendingSource,
12
+ (isPending) => {
13
+ if (isPending) {
14
+ wasPending = true;
15
+ cycleStart = performance.now();
16
+ return;
17
+ }
18
+ if (!wasPending) return;
19
+ wasPending = false;
20
+ const errorVal = errorSource?.value instanceof Error ? errorSource.value : void 0;
21
+ emitHook(nuxtApp, {
22
+ queryName: String(name),
23
+ queryType: "query",
24
+ variables: normalizeVariables(variables),
25
+ durationMs: performance.now() - cycleStart,
26
+ status: errorVal ? "error" : "success",
27
+ ...errorVal ? { error: errorVal } : {}
28
+ });
29
+ },
30
+ { immediate: true }
31
+ );
32
+ return result;
33
+ });
34
+ export const wpMutation = ((...args) => {
35
+ const [name, variables] = args;
36
+ const nuxtApp = useNuxtApp();
37
+ const startTime = performance.now();
38
+ const promise = useGraphqlMutation(...args);
39
+ Promise.resolve(promise).then(
40
+ () => emitHook(nuxtApp, {
41
+ queryName: String(name),
42
+ queryType: "mutation",
43
+ variables: normalizeVariables(variables),
44
+ durationMs: performance.now() - startTime,
45
+ status: "success"
46
+ }),
47
+ (err) => emitHook(nuxtApp, {
48
+ queryName: String(name),
49
+ queryType: "mutation",
50
+ variables: normalizeVariables(variables),
51
+ durationMs: performance.now() - startTime,
52
+ status: "error",
53
+ error: err instanceof Error ? err : new Error(String(err))
54
+ })
55
+ );
56
+ return promise;
57
+ });
58
+ function normalizeVariables(variables) {
59
+ const value = toValue(variables);
60
+ if (value && typeof value === "object") return value;
61
+ return void 0;
62
+ }
63
+ function emitHook(nuxtApp, payload) {
64
+ if (typeof nuxtApp?.callHook !== "function") return;
65
+ const maybePromise = nuxtApp.callHook("wpnuxt:query", payload);
66
+ if (maybePromise && typeof maybePromise.then === "function") {
67
+ Promise.resolve(maybePromise).catch((err) => {
68
+ if (import.meta.dev) {
69
+ console.warn("[wpnuxt] wpnuxt:query hook handler threw", err);
70
+ }
71
+ });
72
+ }
73
+ }
@@ -42,6 +42,11 @@ export function useAsyncGraphqlQuery<T = unknown>(
42
42
  status: Ref<string>
43
43
  }
44
44
  export function useGraphqlState(): Record<string, unknown>
45
+ export function useGraphqlMutation<T = unknown>(
46
+ name: string,
47
+ variables?: Record<string, unknown>,
48
+ options?: Record<string, unknown>
49
+ ): Promise<{ data: T | null, errors?: Array<{ message: string }> | null }>
45
50
 
46
51
  // Stub for #nuxt-graphql-middleware/operation-types
47
52
  export type Query = Record<string, unknown>
@@ -23,7 +23,11 @@ export const transformData = (data, nodes, fixImagePaths) => {
23
23
  if (fixImagePaths && transformedData) {
24
24
  if (Array.isArray(transformedData)) {
25
25
  transformedData.forEach(addRelativePath);
26
- } else {
26
+ } else if (typeof transformedData === "object") {
27
+ const maybeNodes = transformedData.nodes;
28
+ if (Array.isArray(maybeNodes)) {
29
+ maybeNodes.forEach(addRelativePath);
30
+ }
27
31
  addRelativePath(transformedData);
28
32
  }
29
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpnuxt/core",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "description": "Nuxt module for WordPress integration via GraphQL (WPGraphQL)",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -54,7 +54,8 @@
54
54
  "dompurify": "^3.4.0",
55
55
  "graphql": "^16.13.2",
56
56
  "nuxt-graphql-middleware": "5.4.0",
57
- "scule": "^1.3.0"
57
+ "scule": "^1.3.0",
58
+ "typescript": "^5.9.3"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@nuxt/devtools": "^3.2.3",