@wpnuxt/core 2.0.0-beta.4 → 2.0.0-beta.6

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.4",
3
+ "version": "2.0.0-beta.6",
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.4";
11
+ const version = "2.0.0-beta.6";
12
12
 
13
13
  function createModuleError(module, message) {
14
14
  return new Error(formatErrorMessage(module, message));
@@ -152,6 +152,11 @@ function processSelections(selections, level, query, canExtract = true) {
152
152
  if (hasSingleField && canExtract && firstSelection.kind === "Field") {
153
153
  query.nodes?.push(firstSelection.name.value.trim());
154
154
  }
155
+ const hasFragments = selections.some((s) => s.kind === "FragmentSpread");
156
+ const hasCustomFields = selections.some((s) => s.kind === "Field" && s.name.value !== "__typename");
157
+ if (hasFragments && hasCustomFields) {
158
+ query.hasInlineFields = true;
159
+ }
155
160
  selections.forEach((s) => {
156
161
  if (s.kind === "FragmentSpread") {
157
162
  query.fragments?.push(s.name.value.trim());
@@ -216,21 +221,21 @@ async function prepareContext(ctx) {
216
221
  const mutationFnName = (fn) => `useMutation${upperFirst(fn)}`;
217
222
  const formatNodes = (nodes) => nodes?.map((n) => `'${n}'`).join(",") ?? "";
218
223
  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]`;
224
+ if (q.hasInlineFields || !q.fragments?.length) {
225
+ if (q.nodes?.length) {
226
+ let typePath = `${q.name}RootQuery`;
227
+ for (const node of q.nodes) {
228
+ typePath = `NonNullable<${typePath}>['${node}']`;
229
+ }
230
+ if (q.nodes.includes("nodes")) {
231
+ typePath = `${typePath}[number]`;
232
+ }
233
+ return typePath;
230
234
  }
231
- return typePath;
235
+ return `${q.name}RootQuery`;
232
236
  }
233
- return `${q.name}RootQuery`;
237
+ const fragmentSuffix = q.nodes?.includes("nodes") ? "[]" : "";
238
+ return q.fragments.map((f) => `WithImagePath<${f}Fragment>${fragmentSuffix}`).join(" | ");
234
239
  };
235
240
  const queryFnExp = (q, typed = false) => {
236
241
  const functionName = fnName(q.name);
@@ -270,10 +275,10 @@ async function prepareContext(ctx) {
270
275
  const typeSet = /* @__PURE__ */ new Set();
271
276
  queries.forEach((o) => {
272
277
  typeSet.add(`${o.name}QueryVariables`);
273
- if (o.fragments?.length) {
274
- o.fragments.forEach((f) => typeSet.add(`${f}Fragment`));
275
- } else {
278
+ if (o.hasInlineFields || !o.fragments?.length) {
276
279
  typeSet.add(`${o.name}RootQuery`);
280
+ } else {
281
+ o.fragments.forEach((f) => typeSet.add(`${f}Fragment`));
277
282
  }
278
283
  });
279
284
  mutations.forEach((m) => {
@@ -431,6 +436,7 @@ URL: ${fullUrl}
431
436
  Make sure WPGraphQL plugin is installed and activated on your WordPress site.`
432
437
  );
433
438
  }
439
+ await checkWPGraphQLVersion(fullUrl, headers);
434
440
  if (options.schemaPath && !existsSync(options.schemaPath)) {
435
441
  try {
436
442
  const authFlag = options.authToken ? ` -h "Authorization=Bearer ${options.authToken}"` : "";
@@ -500,6 +506,41 @@ Check your wpNuxt.wordpressUrl configuration in nuxt.config.ts`
500
506
  );
501
507
  }
502
508
  }
509
+ async function checkWPGraphQLVersion(fullUrl, headers) {
510
+ try {
511
+ const controller = new AbortController();
512
+ const timeout = setTimeout(() => controller.abort(), 1e4);
513
+ const response = await fetch(fullUrl, {
514
+ method: "POST",
515
+ headers,
516
+ body: JSON.stringify({
517
+ query: '{ __type(name: "MediaDetails") { fields { name } } }'
518
+ }),
519
+ signal: controller.signal
520
+ });
521
+ clearTimeout(timeout);
522
+ if (!response.ok) return;
523
+ const data = await response.json();
524
+ const fields = data?.data?.__type?.fields || [];
525
+ const hasFilePath = fields.some((f) => f.name === "filePath");
526
+ if (!hasFilePath) {
527
+ throw new Error(
528
+ `[wpnuxt:core] WPGraphQL version is too old. WPNuxt v2 requires WPGraphQL >= 2.0.0.
529
+
530
+ URL: ${fullUrl}
531
+
532
+ The installed WPGraphQL version does not support required schema features.
533
+ Please update the WPGraphQL plugin on your WordPress site to version 2.0.0 or later.
534
+
535
+ Download: https://wordpress.org/plugins/wp-graphql/`
536
+ );
537
+ }
538
+ } catch (error) {
539
+ if (error instanceof Error && error.message.startsWith("[wpnuxt:core]")) {
540
+ throw error;
541
+ }
542
+ }
543
+ }
503
544
  const PROBLEMATIC_INTERFACES = /* @__PURE__ */ new Set([
504
545
  "Connection",
505
546
  "Edge",
@@ -809,16 +850,38 @@ const module$1 = defineNuxtModule({
809
850
  });
810
851
  if (wpNuxtConfig.cache?.enabled !== false) {
811
852
  const maxAge = wpNuxtConfig.cache?.maxAge ?? 300;
853
+ const swr = wpNuxtConfig.cache?.swr !== false;
812
854
  const nitroOptions = nuxt.options;
813
855
  nitroOptions.nitro = nitroOptions.nitro || {};
814
856
  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})`);
857
+ const isVercel = process.env.VERCEL === "1" || nitroOptions.nitro.preset === "vercel";
858
+ if (isVercel) {
859
+ const swrValue = swr ? `, stale-while-revalidate=${maxAge}` : "";
860
+ nitroOptions.nitro.routeRules["/api/wpnuxt/query/**"] = {
861
+ headers: {
862
+ "Vercel-CDN-Cache-Control": `s-maxage=${maxAge}${swrValue}`,
863
+ "Vercel-Cache-Tag": "wpnuxt"
864
+ }
865
+ };
866
+ logger.debug(`Vercel CDN caching enabled for GraphQL queries (s-maxage: ${maxAge}s, SWR: ${swr})`);
867
+ } else {
868
+ nitroOptions.nitro.routeRules["/api/wpnuxt/query/**"] = {
869
+ cache: {
870
+ maxAge,
871
+ swr
872
+ }
873
+ };
874
+ logger.debug(`Server-side caching enabled for GraphQL queries (maxAge: ${maxAge}s, SWR: ${swr})`);
875
+ }
876
+ }
877
+ if (wpNuxtConfig.cache?.revalidateSecret) {
878
+ const revalidateHandler = resolver.resolve("./runtime/server/api/wpnuxt/revalidate.post");
879
+ addServerHandler({
880
+ route: "/api/_wpnuxt/revalidate",
881
+ method: "post",
882
+ handler: revalidateHandler
883
+ });
884
+ logger.info("Cache revalidation endpoint registered at POST /api/_wpnuxt/revalidate");
822
885
  }
823
886
  {
824
887
  const nitroOptions = nuxt.options;
@@ -906,7 +969,8 @@ async function loadConfig(options, nuxt) {
906
969
  schemaAuthToken: process.env.WPNUXT_SCHEMA_AUTH_TOKEN,
907
970
  // Only override downloadSchema if env var is explicitly set
908
971
  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
972
+ debug: process.env.WPNUXT_DEBUG ? process.env.WPNUXT_DEBUG === "true" : void 0,
973
+ cache: process.env.WPNUXT_REVALIDATE_SECRET ? { revalidateSecret: process.env.WPNUXT_REVALIDATE_SECRET } : void 0
910
974
  }, options);
911
975
  if (config.downloadSchema === void 0) {
912
976
  config.downloadSchema = true;
@@ -935,6 +999,9 @@ async function loadConfig(options, nuxt) {
935
999
  swr: config.cache?.swr ?? true
936
1000
  }
937
1001
  };
1002
+ if (config.cache?.revalidateSecret) {
1003
+ nuxt.options.runtimeConfig.wpNuxtRevalidateSecret = config.cache.revalidateSecret;
1004
+ }
938
1005
  return config;
939
1006
  }
940
1007
  async function setupServerOptions(nuxt, resolver, logger) {
@@ -1063,6 +1130,12 @@ async function registerModules(nuxt, resolver, wpNuxtConfig, mergedQueriesFolder
1063
1130
  DateTime: "string",
1064
1131
  ID: "string"
1065
1132
  },
1133
+ // Use Record<string, unknown> instead of the default 'object' for unselected
1134
+ // union/interface members. This makes inline fragment types (e.g. ACF relationship
1135
+ // fields) usable without manual type assertions. See: #245
1136
+ output: {
1137
+ emptyObject: "Record<string, unknown>"
1138
+ },
1066
1139
  // Pass auth headers for schema download when token is configured
1067
1140
  ...wpNuxtConfig.schemaAuthToken && {
1068
1141
  urlSchemaOptions: {
@@ -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.4",
3
+ "version": "2.0.0-beta.6",
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"