@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 +15 -0
- package/dist/module.d.ts +15 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +99 -26
- package/dist/runtime/queries/Node.gql +1 -0
- package/dist/runtime/server/api/wpnuxt/revalidate.post.d.ts +0 -0
- package/dist/runtime/server/api/wpnuxt/revalidate.post.js +36 -0
- package/package.json +11 -11
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
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.
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
235
|
+
return `${q.name}RootQuery`;
|
|
232
236
|
}
|
|
233
|
-
|
|
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.
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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: {
|
|
File without changes
|
|
@@ -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.
|
|
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.
|
|
50
|
-
"consola": "^3.4.
|
|
49
|
+
"@nuxt/kit": "4.4.2",
|
|
50
|
+
"consola": "^3.4.2",
|
|
51
51
|
"defu": "^6.1.4",
|
|
52
|
-
"dompurify": "^3.
|
|
53
|
-
"graphql": "^16.
|
|
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.
|
|
58
|
+
"@nuxt/devtools": "^3.2.3",
|
|
59
59
|
"@nuxt/module-builder": "^1.0.2",
|
|
60
|
-
"@nuxt/schema": "4.
|
|
60
|
+
"@nuxt/schema": "4.4.2",
|
|
61
61
|
"@nuxt/test-utils": "^4.0.0",
|
|
62
|
-
"@types/node": "^25.
|
|
63
|
-
"nuxt": "4.
|
|
64
|
-
"vitest": "^4.0
|
|
65
|
-
"vue-tsc": "^3.2.
|
|
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"
|