@wpnuxt/core 2.0.0-beta.5 → 2.0.0-beta.7

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
@@ -111,6 +111,21 @@ interface WPNuxtConfig {
111
111
  * @default true
112
112
  */
113
113
  swr?: boolean;
114
+ /**
115
+ * Secret token for the cache revalidation webhook endpoint.
116
+ *
117
+ * When set, WPNuxt registers a POST endpoint at `/api/_wpnuxt/revalidate`
118
+ * that WordPress can call to purge all cached GraphQL responses immediately.
119
+ *
120
+ * On self-hosted (Node.js), purges Nitro's internal handler cache.
121
+ * On Vercel, also purges the CDN cache when `VERCEL_TOKEN` and
122
+ * `VERCEL_PROJECT_ID` environment variables are set.
123
+ *
124
+ * Can also be set via `WPNUXT_REVALIDATE_SECRET` environment variable.
125
+ *
126
+ * @see https://wpnuxt.com/guide/caching#webhook-revalidation
127
+ */
128
+ revalidateSecret?: string;
114
129
  };
115
130
  }
116
131
 
package/dist/module.d.ts CHANGED
@@ -111,6 +111,21 @@ interface WPNuxtConfig {
111
111
  * @default true
112
112
  */
113
113
  swr?: boolean;
114
+ /**
115
+ * Secret token for the cache revalidation webhook endpoint.
116
+ *
117
+ * When set, WPNuxt registers a POST endpoint at `/api/_wpnuxt/revalidate`
118
+ * that WordPress can call to purge all cached GraphQL responses immediately.
119
+ *
120
+ * On self-hosted (Node.js), purges Nitro's internal handler cache.
121
+ * On Vercel, also purges the CDN cache when `VERCEL_TOKEN` and
122
+ * `VERCEL_PROJECT_ID` environment variables are set.
123
+ *
124
+ * Can also be set via `WPNUXT_REVALIDATE_SECRET` environment variable.
125
+ *
126
+ * @see https://wpnuxt.com/guide/caching#webhook-revalidation
127
+ */
128
+ revalidateSecret?: string;
114
129
  };
115
130
  }
116
131
 
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpnuxt/core",
3
- "version": "2.0.0-beta.5",
3
+ "version": "2.0.0-beta.7",
4
4
  "configKey": "wpNuxt",
5
5
  "compatibility": {
6
6
  "nuxt": ">=3.17.0"
package/dist/module.mjs CHANGED
@@ -2,13 +2,13 @@ import { defu } from 'defu';
2
2
  import { promises, cpSync, existsSync, readdirSync, statSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { writeFile, rename, readFile, mkdir } from 'node:fs/promises';
4
4
  import { join, relative, dirname } from 'node:path';
5
- import { useLogger, createResolver, resolveFiles, defineNuxtModule, addPlugin, addImports, addComponentsDir, addTemplate, addTypeTemplate, hasNuxtModule, installModule } from '@nuxt/kit';
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
8
  import { execSync } from 'node:child_process';
9
9
  import { consola } from 'consola';
10
10
 
11
- const version = "2.0.0-beta.5";
11
+ const version = "2.0.0-beta.7";
12
12
 
13
13
  function createModuleError(module, message) {
14
14
  return new Error(formatErrorMessage(module, message));
@@ -71,9 +71,42 @@ async function mergeQueries(nuxt, wpNuxtConfig, resolver) {
71
71
  logger.debug("Extending queries:", userQueryPath);
72
72
  copyGraphqlFiles(userQueryPath, queryOutputPath);
73
73
  }
74
+ await addCustomFragmentsToNodeQuery(queryOutputPath, userQueryPath, logger);
74
75
  logger.debug("Merged queries folder:", queryOutputPath);
75
76
  return queryOutputPath;
76
77
  }
78
+ const FRAGMENT_DEF_PATTERN = /fragment\s+(\w+)\s+on\s+(\w+)/;
79
+ async function addCustomFragmentsToNodeQuery(queryOutputPath, userQueryPath, logger) {
80
+ 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 });
92
+ }
93
+ }
94
+ if (customFragments.length === 0) return;
95
+ const nodeGqlPath = join(queryOutputPath, "Node.gql");
96
+ if (!existsSync(nodeGqlPath)) return;
97
+ let nodeGql = await promises.readFile(nodeGqlPath, "utf-8");
98
+ const spreads = customFragments.map((f) => ` ... on ${f.type} { ...${f.name} }`).join("\n");
99
+ nodeGql = nodeGql.replace(
100
+ /(\s+\.\.\.Post)\n(\s+\}\n\})/,
101
+ `$1
102
+ ${spreads}
103
+ $2`
104
+ );
105
+ await promises.writeFile(nodeGqlPath, nodeGql, "utf-8");
106
+ logger.debug(
107
+ `Added custom content type fragments to NodeByUri: ${customFragments.map((f) => f.name).join(", ")}`
108
+ );
109
+ }
77
110
  function findConflicts(userQueryPath, outputPath) {
78
111
  const conflicts = [];
79
112
  function walk(dir) {
@@ -152,6 +185,11 @@ function processSelections(selections, level, query, canExtract = true) {
152
185
  if (hasSingleField && canExtract && firstSelection.kind === "Field") {
153
186
  query.nodes?.push(firstSelection.name.value.trim());
154
187
  }
188
+ const hasFragments = selections.some((s) => s.kind === "FragmentSpread");
189
+ const hasCustomFields = selections.some((s) => s.kind === "Field" && s.name.value !== "__typename");
190
+ if (hasFragments && hasCustomFields) {
191
+ query.hasInlineFields = true;
192
+ }
155
193
  selections.forEach((s) => {
156
194
  if (s.kind === "FragmentSpread") {
157
195
  query.fragments?.push(s.name.value.trim());
@@ -216,21 +254,21 @@ async function prepareContext(ctx) {
216
254
  const mutationFnName = (fn) => `useMutation${upperFirst(fn)}`;
217
255
  const formatNodes = (nodes) => nodes?.map((n) => `'${n}'`).join(",") ?? "";
218
256
  const getFragmentType = (q) => {
219
- if (q.fragments?.length) {
220
- const fragmentSuffix = q.nodes?.includes("nodes") ? "[]" : "";
221
- return q.fragments.map((f) => `WithImagePath<${f}Fragment>${fragmentSuffix}`).join(" | ");
222
- }
223
- if (q.nodes?.length) {
224
- let typePath = `${q.name}RootQuery`;
225
- for (const node of q.nodes) {
226
- typePath = `NonNullable<${typePath}>['${node}']`;
227
- }
228
- if (q.nodes.includes("nodes")) {
229
- typePath = `${typePath}[number]`;
257
+ if (q.hasInlineFields || !q.fragments?.length) {
258
+ if (q.nodes?.length) {
259
+ let typePath = `${q.name}RootQuery`;
260
+ for (const node of q.nodes) {
261
+ typePath = `NonNullable<${typePath}>['${node}']`;
262
+ }
263
+ if (q.nodes.includes("nodes")) {
264
+ typePath = `${typePath}[number]`;
265
+ }
266
+ return typePath;
230
267
  }
231
- return typePath;
268
+ return `${q.name}RootQuery`;
232
269
  }
233
- return `${q.name}RootQuery`;
270
+ const fragmentSuffix = q.nodes?.includes("nodes") ? "[]" : "";
271
+ return q.fragments.map((f) => `WithImagePath<${f}Fragment>${fragmentSuffix}`).join(" | ");
234
272
  };
235
273
  const queryFnExp = (q, typed = false) => {
236
274
  const functionName = fnName(q.name);
@@ -270,10 +308,10 @@ async function prepareContext(ctx) {
270
308
  const typeSet = /* @__PURE__ */ new Set();
271
309
  queries.forEach((o) => {
272
310
  typeSet.add(`${o.name}QueryVariables`);
273
- if (o.fragments?.length) {
274
- o.fragments.forEach((f) => typeSet.add(`${f}Fragment`));
275
- } else {
311
+ if (o.hasInlineFields || !o.fragments?.length) {
276
312
  typeSet.add(`${o.name}RootQuery`);
313
+ } else {
314
+ o.fragments.forEach((f) => typeSet.add(`${f}Fragment`));
277
315
  }
278
316
  });
279
317
  mutations.forEach((m) => {
@@ -431,6 +469,7 @@ URL: ${fullUrl}
431
469
  Make sure WPGraphQL plugin is installed and activated on your WordPress site.`
432
470
  );
433
471
  }
472
+ await checkWPGraphQLVersion(fullUrl, headers);
434
473
  if (options.schemaPath && !existsSync(options.schemaPath)) {
435
474
  try {
436
475
  const authFlag = options.authToken ? ` -h "Authorization=Bearer ${options.authToken}"` : "";
@@ -500,6 +539,41 @@ Check your wpNuxt.wordpressUrl configuration in nuxt.config.ts`
500
539
  );
501
540
  }
502
541
  }
542
+ async function checkWPGraphQLVersion(fullUrl, headers) {
543
+ try {
544
+ const controller = new AbortController();
545
+ const timeout = setTimeout(() => controller.abort(), 1e4);
546
+ const response = await fetch(fullUrl, {
547
+ method: "POST",
548
+ headers,
549
+ body: JSON.stringify({
550
+ query: '{ __type(name: "MediaDetails") { fields { name } } }'
551
+ }),
552
+ signal: controller.signal
553
+ });
554
+ clearTimeout(timeout);
555
+ if (!response.ok) return;
556
+ const data = await response.json();
557
+ const fields = data?.data?.__type?.fields || [];
558
+ const hasFilePath = fields.some((f) => f.name === "filePath");
559
+ if (!hasFilePath) {
560
+ throw new Error(
561
+ `[wpnuxt:core] WPGraphQL version is too old. WPNuxt v2 requires WPGraphQL >= 2.0.0.
562
+
563
+ URL: ${fullUrl}
564
+
565
+ The installed WPGraphQL version does not support required schema features.
566
+ Please update the WPGraphQL plugin on your WordPress site to version 2.0.0 or later.
567
+
568
+ Download: https://wordpress.org/plugins/wp-graphql/`
569
+ );
570
+ }
571
+ } catch (error) {
572
+ if (error instanceof Error && error.message.startsWith("[wpnuxt:core]")) {
573
+ throw error;
574
+ }
575
+ }
576
+ }
503
577
  const PROBLEMATIC_INTERFACES = /* @__PURE__ */ new Set([
504
578
  "Connection",
505
579
  "Edge",
@@ -809,16 +883,38 @@ const module$1 = defineNuxtModule({
809
883
  });
810
884
  if (wpNuxtConfig.cache?.enabled !== false) {
811
885
  const maxAge = wpNuxtConfig.cache?.maxAge ?? 300;
886
+ const swr = wpNuxtConfig.cache?.swr !== false;
812
887
  const nitroOptions = nuxt.options;
813
888
  nitroOptions.nitro = nitroOptions.nitro || {};
814
889
  nitroOptions.nitro.routeRules = nitroOptions.nitro.routeRules || {};
815
- nitroOptions.nitro.routeRules["/api/wpnuxt/query/**"] = {
816
- cache: {
817
- maxAge,
818
- swr: wpNuxtConfig.cache?.swr !== false
819
- }
820
- };
821
- logger.debug(`Server-side caching enabled for GraphQL queries (maxAge: ${maxAge}s, SWR: ${wpNuxtConfig.cache?.swr !== false})`);
890
+ const isVercel = process.env.VERCEL === "1" || nitroOptions.nitro.preset === "vercel";
891
+ if (isVercel) {
892
+ const swrValue = swr ? `, stale-while-revalidate=${maxAge}` : "";
893
+ nitroOptions.nitro.routeRules["/api/wpnuxt/query/**"] = {
894
+ headers: {
895
+ "Vercel-CDN-Cache-Control": `s-maxage=${maxAge}${swrValue}`,
896
+ "Vercel-Cache-Tag": "wpnuxt"
897
+ }
898
+ };
899
+ logger.debug(`Vercel CDN caching enabled for GraphQL queries (s-maxage: ${maxAge}s, SWR: ${swr})`);
900
+ } else {
901
+ nitroOptions.nitro.routeRules["/api/wpnuxt/query/**"] = {
902
+ cache: {
903
+ maxAge,
904
+ swr
905
+ }
906
+ };
907
+ logger.debug(`Server-side caching enabled for GraphQL queries (maxAge: ${maxAge}s, SWR: ${swr})`);
908
+ }
909
+ }
910
+ if (wpNuxtConfig.cache?.revalidateSecret) {
911
+ const revalidateHandler = resolver.resolve("./runtime/server/api/wpnuxt/revalidate.post");
912
+ addServerHandler({
913
+ route: "/api/_wpnuxt/revalidate",
914
+ method: "post",
915
+ handler: revalidateHandler
916
+ });
917
+ logger.info("Cache revalidation endpoint registered at POST /api/_wpnuxt/revalidate");
822
918
  }
823
919
  {
824
920
  const nitroOptions = nuxt.options;
@@ -906,7 +1002,8 @@ async function loadConfig(options, nuxt) {
906
1002
  schemaAuthToken: process.env.WPNUXT_SCHEMA_AUTH_TOKEN,
907
1003
  // Only override downloadSchema if env var is explicitly set
908
1004
  downloadSchema: process.env.WPNUXT_DOWNLOAD_SCHEMA !== void 0 ? process.env.WPNUXT_DOWNLOAD_SCHEMA === "true" : void 0,
909
- debug: process.env.WPNUXT_DEBUG ? process.env.WPNUXT_DEBUG === "true" : void 0
1005
+ debug: process.env.WPNUXT_DEBUG ? process.env.WPNUXT_DEBUG === "true" : void 0,
1006
+ cache: process.env.WPNUXT_REVALIDATE_SECRET ? { revalidateSecret: process.env.WPNUXT_REVALIDATE_SECRET } : void 0
910
1007
  }, options);
911
1008
  if (config.downloadSchema === void 0) {
912
1009
  config.downloadSchema = true;
@@ -935,6 +1032,9 @@ async function loadConfig(options, nuxt) {
935
1032
  swr: config.cache?.swr ?? true
936
1033
  }
937
1034
  };
1035
+ if (config.cache?.revalidateSecret) {
1036
+ nuxt.options.runtimeConfig.wpNuxtRevalidateSecret = config.cache.revalidateSecret;
1037
+ }
938
1038
  return config;
939
1039
  }
940
1040
  async function setupServerOptions(nuxt, resolver, logger) {
@@ -53,11 +53,12 @@ export const useWPContent = (queryName, nodes, fixImagePaths, params, options) =
53
53
  }
54
54
  }
55
55
  };
56
- const { data, pending, refresh, execute, clear, error, status } = useAsyncGraphqlQuery(
56
+ const asyncResult = useAsyncGraphqlQuery(
57
57
  String(queryName),
58
58
  normalizedParams ?? {},
59
59
  asyncDataOptions
60
60
  );
61
+ const { data, pending, refresh, execute, clear, error, status } = asyncResult;
61
62
  const transformError = ref(null);
62
63
  if (timeoutId !== void 0) {
63
64
  vueWatch(pending, (isPending) => {
@@ -118,7 +119,7 @@ See: https://wpnuxt.com/guide/menus`
118
119
  return void 0;
119
120
  }
120
121
  });
121
- return {
122
+ const returnValue = {
122
123
  data: transformedData,
123
124
  pending,
124
125
  refresh,
@@ -133,4 +134,9 @@ See: https://wpnuxt.com/guide/menus`
133
134
  /** Whether a retry is currently in progress */
134
135
  isRetrying
135
136
  };
137
+ const thenable = asyncResult;
138
+ return Object.assign(
139
+ thenable.then(() => returnValue),
140
+ returnValue
141
+ );
136
142
  };
@@ -1,5 +1,6 @@
1
1
  query NodeByUri($uri: String!) {
2
2
  nodeByUri(uri: $uri) {
3
+ __typename
3
4
  ...Page
4
5
  ...Post
5
6
  }
@@ -0,0 +1,36 @@
1
+ import { createConsola } from "consola";
2
+ import { defineEventHandler, readBody, createError } from "h3";
3
+ const logger = createConsola().withTag("wpnuxt");
4
+ export default defineEventHandler(async (event) => {
5
+ const body = await readBody(event);
6
+ const { wpNuxtRevalidateSecret } = useRuntimeConfig(event);
7
+ if (!wpNuxtRevalidateSecret || body?.secret !== wpNuxtRevalidateSecret) {
8
+ throw createError({ statusCode: 401, statusMessage: "Invalid secret" });
9
+ }
10
+ const storage = useStorage("cache:nitro:handlers");
11
+ const keys = await storage.getKeys();
12
+ const wpnuxtKeys = keys.filter((k) => k.includes("wpnuxt"));
13
+ await Promise.all(wpnuxtKeys.map((key) => storage.removeItem(key)));
14
+ let vercelPurged = false;
15
+ if (process.env.VERCEL && process.env.VERCEL_TOKEN && process.env.VERCEL_PROJECT_ID) {
16
+ try {
17
+ const response = await fetch(`https://api.vercel.com/v1/edge-cache/invalidate-by-tags?projectIdOrName=${process.env.VERCEL_PROJECT_ID}`, {
18
+ method: "POST",
19
+ headers: {
20
+ "Authorization": `Bearer ${process.env.VERCEL_TOKEN}`,
21
+ "Content-Type": "application/json"
22
+ },
23
+ body: JSON.stringify({ tags: ["wpnuxt"] })
24
+ });
25
+ vercelPurged = response.ok;
26
+ if (!response.ok) {
27
+ logger.warn(`Vercel cache purge failed: ${response.status} ${response.statusText}`);
28
+ }
29
+ } catch (error) {
30
+ logger.warn("Vercel cache purge request failed:", error);
31
+ }
32
+ }
33
+ const purged = wpnuxtKeys.length;
34
+ logger.info(`Cache revalidated: purged ${purged} Nitro entries${vercelPurged ? ", Vercel CDN cache purged" : ""}`);
35
+ return { success: true, purged, vercelPurged };
36
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpnuxt/core",
3
- "version": "2.0.0-beta.5",
3
+ "version": "2.0.0-beta.7",
4
4
  "description": "Nuxt module for WordPress integration via GraphQL (WPGraphQL)",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -46,23 +46,23 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@nuxt/kit": "4.3.1",
50
- "consola": "^3.4.0",
49
+ "@nuxt/kit": "4.4.2",
50
+ "consola": "^3.4.2",
51
51
  "defu": "^6.1.4",
52
- "dompurify": "^3.1.7",
53
- "graphql": "^16.12.0",
52
+ "dompurify": "^3.3.3",
53
+ "graphql": "^16.13.1",
54
54
  "nuxt-graphql-middleware": "5.3.2",
55
55
  "scule": "^1.3.0"
56
56
  },
57
57
  "devDependencies": {
58
- "@nuxt/devtools": "^3.2.1",
58
+ "@nuxt/devtools": "^3.2.3",
59
59
  "@nuxt/module-builder": "^1.0.2",
60
- "@nuxt/schema": "4.3.1",
60
+ "@nuxt/schema": "4.4.2",
61
61
  "@nuxt/test-utils": "^4.0.0",
62
- "@types/node": "^25.2.3",
63
- "nuxt": "4.3.1",
64
- "vitest": "^4.0.18",
65
- "vue-tsc": "^3.2.3"
62
+ "@types/node": "^25.5.0",
63
+ "nuxt": "^4.4.2",
64
+ "vitest": "^4.1.0",
65
+ "vue-tsc": "^3.2.5"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "nuxt": ">=3.17.0"