@wpnuxt/core 2.0.0-alpha.7 → 2.0.0-alpha.9

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
@@ -30,6 +30,11 @@ interface WPNuxtConfig {
30
30
  * @default '.queries/'
31
31
  */
32
32
  mergedOutputFolder: string;
33
+ /**
34
+ * Whether to warn when a user query file overrides a default query file
35
+ * @default true
36
+ */
37
+ warnOnOverride?: boolean;
33
38
  };
34
39
  /**
35
40
  * Whether to download the schema from the WordPress site and save it to disk
package/dist/module.d.ts CHANGED
@@ -30,6 +30,11 @@ interface WPNuxtConfig {
30
30
  * @default '.queries/'
31
31
  */
32
32
  mergedOutputFolder: string;
33
+ /**
34
+ * Whether to warn when a user query file overrides a default query file
35
+ * @default true
36
+ */
37
+ warnOnOverride?: boolean;
33
38
  };
34
39
  /**
35
40
  * Whether to download the schema from the WordPress site and save it to disk
package/dist/module.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@wpnuxt/core",
3
+ "version": "2.0.0-alpha.8",
3
4
  "configKey": "wpNuxt",
4
5
  "compatibility": {
5
6
  "nuxt": ">=3.0.0"
6
7
  },
7
- "version": "2.0.0-alpha.6",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,12 +1,15 @@
1
1
  import { defu } from 'defu';
2
- import { promises, cpSync, existsSync, statSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { mkdir, writeFile } from 'node:fs/promises';
4
- import { join } from 'node:path';
2
+ import { promises, cpSync, existsSync, readdirSync, statSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { writeFile, rename, readFile, mkdir } from 'node:fs/promises';
4
+ import { join, relative, dirname } from 'node:path';
5
5
  import { useLogger, createResolver, resolveFiles, defineNuxtModule, addPlugin, addImports, addComponentsDir, addTemplate, addTypeTemplate, hasNuxtModule, installModule } from '@nuxt/kit';
6
6
  import { upperFirst } from 'scule';
7
7
  import { ref } from 'vue';
8
8
  import { parse, GraphQLError } from 'graphql';
9
9
  import { execSync } from 'node:child_process';
10
+ import { consola } from 'consola';
11
+
12
+ const version = "2.0.0-alpha.8";
10
13
 
11
14
  function createModuleError(module, message) {
12
15
  return new Error(formatErrorMessage(module, message));
@@ -15,6 +18,29 @@ function formatErrorMessage(module, message) {
15
18
  return `[wpnuxt:${module}] ${message}`;
16
19
  }
17
20
 
21
+ function validateWordPressUrl(url) {
22
+ if (!url?.trim()) {
23
+ return { valid: false, error: "URL cannot be empty" };
24
+ }
25
+ let normalizedUrl = url.trim();
26
+ if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
27
+ normalizedUrl = `https://${normalizedUrl}`;
28
+ }
29
+ try {
30
+ const parsed = new URL(normalizedUrl);
31
+ if (!["http:", "https:"].includes(parsed.protocol)) {
32
+ return { valid: false, error: "URL must use http or https protocol" };
33
+ }
34
+ return { valid: true, normalizedUrl: normalizedUrl.replace(/\/+$/, "") };
35
+ } catch {
36
+ return { valid: false, error: "Invalid URL format" };
37
+ }
38
+ }
39
+ async function atomicWriteFile(path, content) {
40
+ const tempPath = `${path}.${Date.now()}.tmp`;
41
+ await writeFile(tempPath, content, "utf-8");
42
+ await rename(tempPath, path);
43
+ }
18
44
  function randHashGenerator(length = 12) {
19
45
  return Math.random().toString(36).substring(2, 2 + length).toUpperCase().padEnd(length, "0");
20
46
  }
@@ -35,6 +61,13 @@ async function mergeQueries(nuxt, wpNuxtConfig, resolver) {
35
61
  const defaultQueriesPath = resolver.resolve("./runtime/queries");
36
62
  await promises.rm(queryOutputPath, { recursive: true, force: true });
37
63
  cpSync(defaultQueriesPath, queryOutputPath, { recursive: true });
64
+ const conflicts = findConflicts(userQueryPath, queryOutputPath);
65
+ if (conflicts.length && wpNuxtConfig.queries.warnOnOverride) {
66
+ logger.warn("The following user query files will override default queries:");
67
+ for (const file of conflicts) {
68
+ logger.warn(` - ${file}`);
69
+ }
70
+ }
38
71
  if (existsSync(userQueryPath)) {
39
72
  logger.debug("Extending queries:", userQueryPath);
40
73
  cpSync(userQueryPath, queryOutputPath, { recursive: true });
@@ -42,6 +75,29 @@ async function mergeQueries(nuxt, wpNuxtConfig, resolver) {
42
75
  logger.debug("Merged queries folder:", queryOutputPath);
43
76
  return queryOutputPath;
44
77
  }
78
+ function findConflicts(userQueryPath, outputPath) {
79
+ const conflicts = [];
80
+ function walk(dir) {
81
+ const entries = readdirSync(dir);
82
+ for (const entry of entries) {
83
+ const fullPath = join(dir, entry);
84
+ const stat = statSync(fullPath);
85
+ if (stat.isDirectory()) {
86
+ walk(fullPath);
87
+ } else if (stat.isFile()) {
88
+ const rel = relative(userQueryPath, fullPath);
89
+ const target = join(outputPath, rel);
90
+ if (existsSync(target)) {
91
+ conflicts.push(rel);
92
+ }
93
+ }
94
+ }
95
+ }
96
+ if (existsSync(userQueryPath)) {
97
+ walk(userQueryPath);
98
+ }
99
+ return conflicts;
100
+ }
45
101
 
46
102
  const _parseDoc = async (doc) => {
47
103
  if (!doc || typeof doc !== "string" || doc.trim().length === 0) {
@@ -143,8 +199,21 @@ async function prepareContext(ctx) {
143
199
  const mutationFnName = (fn) => `useMutation${upperFirst(fn)}`;
144
200
  const formatNodes = (nodes) => nodes?.map((n) => `'${n}'`).join(",") ?? "";
145
201
  const getFragmentType = (q) => {
146
- const fragmentSuffix = q.fragments?.length && q.nodes?.includes("nodes") ? "[]" : "";
147
- return q.fragments?.length ? q.fragments.map((f) => `${f}Fragment${fragmentSuffix}`).join(" | ") : "any";
202
+ if (q.fragments?.length) {
203
+ const fragmentSuffix = q.nodes?.includes("nodes") ? "[]" : "";
204
+ return q.fragments.map((f) => `${f}Fragment${fragmentSuffix}`).join(" | ");
205
+ }
206
+ if (q.nodes?.length) {
207
+ let typePath = `${q.name}RootQuery`;
208
+ for (const node of q.nodes) {
209
+ typePath = `NonNullable<${typePath}>['${node}']`;
210
+ }
211
+ if (q.nodes.includes("nodes")) {
212
+ typePath = `${typePath}[number]`;
213
+ }
214
+ return typePath;
215
+ }
216
+ return `${q.name}RootQuery`;
148
217
  };
149
218
  const queryFnExp = (q, typed = false) => {
150
219
  const functionName = fnName(q.name);
@@ -161,19 +230,34 @@ async function prepareContext(ctx) {
161
230
  return ` export const ${functionName}: (variables: ${m.name}MutationVariables, options?: WPMutationOptions) => Promise<WPMutationResult<${m.name}Mutation>>`;
162
231
  };
163
232
  ctx.generateImports = () => {
233
+ const lines = [];
164
234
  const imports = [];
235
+ if (queries.length > 0) {
236
+ imports.push("useWPContent");
237
+ }
238
+ if (mutations.length > 0) {
239
+ imports.push("useGraphqlMutation");
240
+ }
241
+ if (imports.length > 0) {
242
+ lines.push(`import { ${imports.join(", ")} } from '#imports'`);
243
+ lines.push("");
244
+ }
165
245
  queries.forEach((f) => {
166
- imports.push(queryFnExp(f, false));
246
+ lines.push(queryFnExp(f, false));
167
247
  });
168
248
  mutations.forEach((m) => {
169
- imports.push(mutationFnExp(m, false));
249
+ lines.push(mutationFnExp(m, false));
170
250
  });
171
- return imports.join("\n");
251
+ return lines.join("\n");
172
252
  };
173
253
  const typeSet = /* @__PURE__ */ new Set();
174
254
  queries.forEach((o) => {
175
255
  typeSet.add(`${o.name}QueryVariables`);
176
- o.fragments?.forEach((f) => typeSet.add(`${f}Fragment`));
256
+ if (o.fragments?.length) {
257
+ o.fragments.forEach((f) => typeSet.add(`${f}Fragment`));
258
+ } else {
259
+ typeSet.add(`${o.name}RootQuery`);
260
+ }
177
261
  });
178
262
  mutations.forEach((m) => {
179
263
  typeSet.add(`${m.name}MutationVariables`);
@@ -404,9 +488,289 @@ function patchWPGraphQLSchema(schemaPath) {
404
488
  writeFileSync(schemaPath, schema);
405
489
  }
406
490
 
491
+ async function runInstall(nuxt) {
492
+ const logger = useLogger("wpnuxt", {
493
+ level: process.env.WPNUXT_DEBUG === "true" ? 4 : 3
494
+ });
495
+ const results = [];
496
+ results.push(await setupEnvFiles(nuxt, logger));
497
+ const parallel = await Promise.all([
498
+ setupMcpConfig(nuxt, logger),
499
+ setupGitignore(nuxt, logger),
500
+ setupQueriesFolder(nuxt, logger)
501
+ ]);
502
+ results.push(...parallel);
503
+ displayInstallSummary(results, logger);
504
+ }
505
+ function displayInstallSummary(results, logger) {
506
+ const successes = results.filter((r) => r.success && !r.skipped);
507
+ const skipped = results.filter((r) => r.skipped);
508
+ const failures = results.filter((r) => !r.success);
509
+ if (successes.length > 0) {
510
+ const lines = [
511
+ ...successes.map((r) => `\u2713 ${r.message || r.name}`),
512
+ "",
513
+ "Next steps:",
514
+ " 1. Ensure WPGraphQL is installed on WordPress",
515
+ " 2. Run `pnpm dev` to start development",
516
+ "",
517
+ "Docs: https://wpnuxt.com"
518
+ ];
519
+ consola.box({
520
+ title: "WPNuxt Setup Complete",
521
+ message: lines.join("\n")
522
+ });
523
+ }
524
+ if (skipped.length > 0) {
525
+ logger.debug(`Skipped (already configured): ${skipped.map((r) => r.name).join(", ")}`);
526
+ }
527
+ if (failures.length > 0) {
528
+ logger.debug(`Failed: ${failures.map((r) => r.name).join(", ")}`);
529
+ }
530
+ }
531
+ const ENV_EXAMPLE_CONTENT = `# WPNuxt Configuration
532
+ # Required: Your WordPress site URL (must have WPGraphQL plugin installed)
533
+ WPNUXT_WORDPRESS_URL=https://your-wordpress-site.com
534
+
535
+ # Optional: Custom GraphQL endpoint (default: /graphql)
536
+ # WPNUXT_GRAPHQL_ENDPOINT=/graphql
537
+
538
+ # Optional: Enable debug mode for verbose logging
539
+ # WPNUXT_DEBUG=true
540
+ `;
541
+ function isInteractiveEnvironment() {
542
+ return !(process.env.CI === "true" || process.env.CI === "1" || process.env.VITEST === "true" || process.env.TEST === "true" || process.env.NODE_ENV === "test");
543
+ }
544
+ async function checkExistingEnvConfig(nuxt, envPath) {
545
+ const nuxtConfig = nuxt.options;
546
+ if (nuxtConfig.wpNuxt?.wordpressUrl) {
547
+ return { hasUrl: true, source: "nuxt.config.ts", envContent: "" };
548
+ }
549
+ if (process.env.WPNUXT_WORDPRESS_URL) {
550
+ return { hasUrl: true, source: "WPNUXT_WORDPRESS_URL env var", envContent: "" };
551
+ }
552
+ if (existsSync(envPath)) {
553
+ const envContent = await readFile(envPath, "utf-8");
554
+ if (/^WPNUXT_WORDPRESS_URL\s*=\s*.+/m.test(envContent)) {
555
+ return { hasUrl: true, source: ".env file", envContent };
556
+ }
557
+ return { hasUrl: false, envContent };
558
+ }
559
+ return { hasUrl: false, envContent: "" };
560
+ }
561
+ async function setupEnvFiles(nuxt, logger) {
562
+ const envPath = join(nuxt.options.rootDir, ".env");
563
+ const envExamplePath = join(nuxt.options.rootDir, ".env.example");
564
+ try {
565
+ const existingConfig = await checkExistingEnvConfig(nuxt, envPath);
566
+ let envContent = existingConfig.envContent;
567
+ let urlConfigured = false;
568
+ if (existingConfig.hasUrl) {
569
+ logger.debug(`WordPress URL already configured in ${existingConfig.source}`);
570
+ urlConfigured = true;
571
+ } else if (isInteractiveEnvironment()) {
572
+ consola.box({
573
+ title: "WPNuxt Setup",
574
+ message: "Configure your WordPress connection"
575
+ });
576
+ const wordpressUrl = await consola.prompt(
577
+ "What is your WordPress site URL?",
578
+ {
579
+ type: "text",
580
+ placeholder: "https://your-wordpress-site.com",
581
+ initial: ""
582
+ }
583
+ );
584
+ if (wordpressUrl && typeof wordpressUrl === "string" && wordpressUrl.trim()) {
585
+ const validation = validateWordPressUrl(wordpressUrl);
586
+ if (!validation.valid) {
587
+ logger.warn(`Invalid URL: ${validation.error}`);
588
+ logger.info("Skipped WordPress URL configuration. Add WPNUXT_WORDPRESS_URL to your .env file later.");
589
+ } else {
590
+ const envLine = `WPNUXT_WORDPRESS_URL=${validation.normalizedUrl}
591
+ `;
592
+ if (envContent) {
593
+ envContent = envContent.trimEnd() + "\n\n" + envLine;
594
+ } else {
595
+ envContent = envLine;
596
+ }
597
+ await atomicWriteFile(envPath, envContent);
598
+ logger.success(`WordPress URL saved to .env: ${validation.normalizedUrl}`);
599
+ urlConfigured = true;
600
+ }
601
+ } else {
602
+ logger.info("Skipped WordPress URL configuration. Add WPNUXT_WORDPRESS_URL to your .env file later.");
603
+ }
604
+ } else {
605
+ logger.debug("Non-interactive environment detected, skipping WordPress URL prompt");
606
+ }
607
+ let exampleContent = "";
608
+ let exampleUpdated = false;
609
+ if (existsSync(envExamplePath)) {
610
+ exampleContent = await readFile(envExamplePath, "utf-8");
611
+ if (exampleContent.includes("WPNUXT_WORDPRESS_URL")) {
612
+ logger.debug(".env.example already includes WPNuxt configuration");
613
+ } else {
614
+ exampleContent = exampleContent.trimEnd() + "\n\n" + ENV_EXAMPLE_CONTENT;
615
+ await atomicWriteFile(envExamplePath, exampleContent);
616
+ exampleUpdated = true;
617
+ }
618
+ } else {
619
+ exampleContent = ENV_EXAMPLE_CONTENT;
620
+ await atomicWriteFile(envExamplePath, exampleContent);
621
+ exampleUpdated = true;
622
+ }
623
+ if (urlConfigured && exampleUpdated) {
624
+ return {
625
+ name: "Environment files",
626
+ success: true,
627
+ message: "Configured .env with WordPress URL"
628
+ };
629
+ } else if (urlConfigured) {
630
+ return { name: "Environment files", success: true, skipped: true };
631
+ } else if (exampleUpdated) {
632
+ return {
633
+ name: "Environment files",
634
+ success: true,
635
+ message: "Created .env.example template"
636
+ };
637
+ } else {
638
+ return { name: "Environment files", success: true, skipped: true };
639
+ }
640
+ } catch (error) {
641
+ const message = error instanceof Error ? error.message : String(error);
642
+ logger.warn(`Failed to setup environment files: ${message}`);
643
+ return { name: "Environment files", success: false, message };
644
+ }
645
+ }
646
+ const MCP_CONFIG = {
647
+ wpnuxt: {
648
+ type: "http",
649
+ url: "https://wpnuxt.com/mcp"
650
+ }
651
+ };
652
+ async function setupMcpConfig(nuxt, logger) {
653
+ const mcpPath = join(nuxt.options.rootDir, ".mcp.json");
654
+ try {
655
+ let config = { mcpServers: {} };
656
+ if (existsSync(mcpPath)) {
657
+ const content = await readFile(mcpPath, "utf-8");
658
+ config = JSON.parse(content);
659
+ config.mcpServers = config.mcpServers || {};
660
+ if (config.mcpServers.wpnuxt) {
661
+ logger.debug("MCP config already includes wpnuxt server");
662
+ return { name: "MCP config", success: true, skipped: true };
663
+ }
664
+ }
665
+ config.mcpServers = {
666
+ ...config.mcpServers,
667
+ ...MCP_CONFIG
668
+ };
669
+ await atomicWriteFile(mcpPath, JSON.stringify(config, null, 2) + "\n");
670
+ return {
671
+ name: "MCP config",
672
+ success: true,
673
+ message: "Created .mcp.json with WPNuxt MCP server"
674
+ };
675
+ } catch (error) {
676
+ const message = error instanceof Error ? error.message : String(error);
677
+ logger.warn(`Failed to setup MCP configuration: ${message}`);
678
+ return { name: "MCP config", success: false, message };
679
+ }
680
+ }
681
+ async function setupGitignore(nuxt, logger) {
682
+ const gitignorePath = join(nuxt.options.rootDir, ".gitignore");
683
+ try {
684
+ let content = "";
685
+ if (existsSync(gitignorePath)) {
686
+ content = await readFile(gitignorePath, "utf-8");
687
+ if (content.includes(".queries")) {
688
+ logger.debug(".gitignore already includes .queries");
689
+ return { name: "Gitignore", success: true, skipped: true };
690
+ }
691
+ content = content.trimEnd() + "\n\n# WPNuxt generated files\n.queries/\n";
692
+ } else {
693
+ content = "# WPNuxt generated files\n.queries/\n";
694
+ }
695
+ await atomicWriteFile(gitignorePath, content);
696
+ return {
697
+ name: "Gitignore",
698
+ success: true,
699
+ message: "Added .queries/ to .gitignore"
700
+ };
701
+ } catch (error) {
702
+ const message = error instanceof Error ? error.message : String(error);
703
+ logger.warn(`Failed to setup .gitignore: ${message}`);
704
+ return { name: "Gitignore", success: false, message };
705
+ }
706
+ }
707
+ const QUERIES_README = `# Custom GraphQL Queries
708
+
709
+ Place your custom \`.gql\` or \`.graphql\` files here to extend or override the default WPNuxt queries.
710
+
711
+ ## How it works
712
+
713
+ 1. Files here are merged with WPNuxt's default queries during build
714
+ 2. If a file has the same name as a default query, yours will override it
715
+ 3. New files will generate new composables automatically
716
+
717
+ ## Example
718
+
719
+ Create a file \`CustomPosts.gql\`:
720
+
721
+ \`\`\`graphql
722
+ query CustomPosts($first: Int = 10) {
723
+ posts(first: $first) {
724
+ nodes {
725
+ id
726
+ title
727
+ date
728
+ # Add your custom fields here
729
+ }
730
+ }
731
+ }
732
+ \`\`\`
733
+
734
+ This generates \`useCustomPosts()\` and \`useAsyncCustomPosts()\` composables.
735
+
736
+ ## Available Fragments
737
+
738
+ You can use these fragments from WPNuxt's defaults:
739
+ - \`...Post\` - Standard post fields
740
+ - \`...Page\` - Standard page fields
741
+ - \`...ContentNode\` - Common content fields
742
+ - \`...FeaturedImage\` - Featured image with sizes
743
+
744
+ ## Documentation
745
+
746
+ See https://wpnuxt.com/guide/custom-queries for more details.
747
+ `;
748
+ async function setupQueriesFolder(nuxt, logger) {
749
+ const queriesPath = join(nuxt.options.rootDir, "extend", "queries");
750
+ const readmePath = join(queriesPath, "README.md");
751
+ try {
752
+ if (existsSync(readmePath)) {
753
+ logger.debug("extend/queries/ folder already exists");
754
+ return { name: "Queries folder", success: true, skipped: true };
755
+ }
756
+ await mkdir(queriesPath, { recursive: true });
757
+ await atomicWriteFile(readmePath, QUERIES_README);
758
+ return {
759
+ name: "Queries folder",
760
+ success: true,
761
+ message: "Created extend/queries/ for custom GraphQL queries"
762
+ };
763
+ } catch (error) {
764
+ const message = error instanceof Error ? error.message : String(error);
765
+ logger.warn(`Failed to setup extend/queries/ folder: ${message}`);
766
+ return { name: "Queries folder", success: false, message };
767
+ }
768
+ }
769
+
407
770
  const module$1 = defineNuxtModule({
408
771
  meta: {
409
772
  name: "@wpnuxt/core",
773
+ version,
410
774
  configKey: "wpNuxt",
411
775
  compatibility: {
412
776
  nuxt: ">=3.0.0"
@@ -417,7 +781,8 @@ const module$1 = defineNuxtModule({
417
781
  graphqlEndpoint: "/graphql",
418
782
  queries: {
419
783
  extendFolder: "extend/queries/",
420
- mergedOutputFolder: ".queries/"
784
+ mergedOutputFolder: ".queries/",
785
+ warnOnOverride: true
421
786
  },
422
787
  downloadSchema: true,
423
788
  debug: false,
@@ -437,6 +802,7 @@ const module$1 = defineNuxtModule({
437
802
  nuxt.options.runtimeConfig.public.buildHash = randHashGenerator();
438
803
  addPlugin(resolver.resolve("./runtime/plugins/graphqlConfig"));
439
804
  addPlugin(resolver.resolve("./runtime/plugins/graphqlErrors"));
805
+ configureTrailingSlash(nuxt, logger);
440
806
  const mergedQueriesFolder = await mergeQueries(nuxt, wpNuxtConfig, resolver);
441
807
  await setupServerOptions(nuxt, resolver, logger);
442
808
  await setupClientOptions(nuxt, resolver, logger);
@@ -477,8 +843,10 @@ const module$1 = defineNuxtModule({
477
843
  });
478
844
  if (wpNuxtConfig.cache?.enabled !== false) {
479
845
  const maxAge = wpNuxtConfig.cache?.maxAge ?? 300;
480
- nuxt.options.nitro.routeRules = nuxt.options.nitro.routeRules || {};
481
- nuxt.options.nitro.routeRules["/api/wpnuxt/**"] = {
846
+ const nitroOptions = nuxt.options;
847
+ nitroOptions.nitro = nitroOptions.nitro || {};
848
+ nitroOptions.nitro.routeRules = nitroOptions.nitro.routeRules || {};
849
+ nitroOptions.nitro.routeRules["/api/wpnuxt/**"] = {
482
850
  cache: {
483
851
  maxAge,
484
852
  swr: wpNuxtConfig.cache?.swr !== false
@@ -510,10 +878,12 @@ const module$1 = defineNuxtModule({
510
878
  nuxt.options.alias["#wpnuxt"] = resolver.resolve(nuxt.options.buildDir, "wpnuxt");
511
879
  nuxt.options.alias["#wpnuxt/*"] = resolver.resolve(nuxt.options.buildDir, "wpnuxt", "*");
512
880
  nuxt.options.alias["#wpnuxt/types"] = resolver.resolve("./types");
513
- nuxt.options.nitro.alias = nuxt.options.nitro.alias || {};
514
- nuxt.options.nitro.alias["#wpnuxt/types"] = resolver.resolve("./types");
515
- nuxt.options.nitro.externals = nuxt.options.nitro.externals || {};
516
- nuxt.options.nitro.externals.inline = nuxt.options.nitro.externals.inline || [];
881
+ const nitroOpts = nuxt.options;
882
+ nitroOpts.nitro = nitroOpts.nitro || {};
883
+ nitroOpts.nitro.alias = nitroOpts.nitro.alias || {};
884
+ nitroOpts.nitro.alias["#wpnuxt/types"] = resolver.resolve("./types");
885
+ nitroOpts.nitro.externals = nitroOpts.nitro.externals || {};
886
+ nitroOpts.nitro.externals.inline = nitroOpts.nitro.externals.inline || [];
517
887
  addTemplate({
518
888
  write: true,
519
889
  filename: "wpnuxt/index.mjs",
@@ -529,6 +899,9 @@ const module$1 = defineNuxtModule({
529
899
  });
530
900
  logger.trace("Finished generating composables");
531
901
  logger.info(`WPNuxt module loaded in ${(/* @__PURE__ */ new Date()).getTime() - startTime}ms`);
902
+ },
903
+ async onInstall(nuxt) {
904
+ await runInstall(nuxt);
532
905
  }
533
906
  });
534
907
  function loadConfig(options, nuxt) {
@@ -661,18 +1034,63 @@ async function setupClientOptions(nuxt, _resolver, logger) {
661
1034
  await writeFile(targetPath, CLIENT_OPTIONS_TEMPLATE);
662
1035
  logger.debug("Created graphqlMiddleware.clientOptions.ts with WPNuxt defaults (preview mode support)");
663
1036
  }
1037
+ function configureTrailingSlash(nuxt, logger) {
1038
+ const handlerPath = join(nuxt.options.buildDir, "wpnuxt", "trailing-slash-handler.ts");
1039
+ const handlerCode = `import { defineEventHandler, sendRedirect, getRequestURL } from 'h3'
1040
+
1041
+ export default defineEventHandler((event) => {
1042
+ const url = getRequestURL(event)
1043
+ const path = url.pathname
1044
+
1045
+ // Skip if:
1046
+ // - Already has trailing slash
1047
+ // - Is root path
1048
+ // - Is an API route
1049
+ // - Has a file extension (likely a static file)
1050
+ // - Is a Nuxt internal route (_nuxt, __nuxt)
1051
+ if (
1052
+ path.endsWith('/') ||
1053
+ path === '' ||
1054
+ path.startsWith('/api/') ||
1055
+ path.startsWith('/_nuxt/') ||
1056
+ path.startsWith('/__nuxt') ||
1057
+ path.includes('.')
1058
+ ) {
1059
+ return
1060
+ }
1061
+
1062
+ // Redirect to trailing slash version
1063
+ return sendRedirect(event, path + '/' + url.search, 301)
1064
+ })
1065
+ `;
1066
+ nuxt.hook("build:before", async () => {
1067
+ await mkdir(dirname(handlerPath), { recursive: true });
1068
+ await writeFile(handlerPath, handlerCode);
1069
+ logger.debug("Created trailing slash handler at " + handlerPath);
1070
+ });
1071
+ nuxt.hook("nitro:config", (nitroConfig) => {
1072
+ nitroConfig.handlers = nitroConfig.handlers || [];
1073
+ nitroConfig.handlers.unshift({
1074
+ route: "/**",
1075
+ handler: handlerPath
1076
+ });
1077
+ });
1078
+ logger.debug("Configured trailing slash handling for WordPress URI compatibility");
1079
+ }
664
1080
  function configureVercelSettings(nuxt, logger) {
665
- const isVercel = process.env.VERCEL === "1" || nuxt.options.nitro.preset === "vercel";
1081
+ const opts = nuxt.options;
1082
+ opts.nitro = opts.nitro || {};
1083
+ const isVercel = process.env.VERCEL === "1" || opts.nitro.preset === "vercel";
666
1084
  if (isVercel) {
667
1085
  logger.debug("Vercel deployment detected, applying recommended settings");
668
- nuxt.options.nitro.future = nuxt.options.nitro.future || {};
669
- if (nuxt.options.nitro.future.nativeSWR === void 0) {
670
- nuxt.options.nitro.future.nativeSWR = true;
1086
+ opts.nitro.future = opts.nitro.future || {};
1087
+ if (opts.nitro.future.nativeSWR === void 0) {
1088
+ opts.nitro.future.nativeSWR = true;
671
1089
  logger.debug("Enabled nitro.future.nativeSWR for Vercel ISR compatibility");
672
1090
  }
673
- nuxt.options.routeRules = nuxt.options.routeRules || {};
674
- if (!nuxt.options.routeRules["/**"]) {
675
- nuxt.options.routeRules["/**"] = { ssr: true };
1091
+ opts.routeRules = opts.routeRules || {};
1092
+ if (!opts.routeRules["/**"]) {
1093
+ opts.routeRules["/**"] = { ssr: true };
676
1094
  logger.debug("Enabled SSR for all routes (routeRules['/**'] = { ssr: true })");
677
1095
  }
678
1096
  }
@@ -1,4 +1,4 @@
1
- import { getRelativeImagePath } from "../util/images.js";
1
+ import { transformData, normalizeUriParam } from "../util/content.js";
2
2
  import { computed, ref, watch as vueWatch, useAsyncGraphqlQuery } from "#imports";
3
3
  export const useWPContent = (queryName, nodes, fixImagePaths, params, options) => {
4
4
  const {
@@ -9,6 +9,7 @@ export const useWPContent = (queryName, nodes, fixImagePaths, params, options) =
9
9
  timeout: timeoutOption,
10
10
  ...restOptions
11
11
  } = options ?? {};
12
+ const normalizedParams = normalizeUriParam(params);
12
13
  const maxRetries = retryOption === false ? 0 : retryOption ?? 0;
13
14
  const baseRetryDelay = retryDelayOption ?? 1e3;
14
15
  const retryCount = ref(0);
@@ -50,7 +51,7 @@ export const useWPContent = (queryName, nodes, fixImagePaths, params, options) =
50
51
  };
51
52
  const { data, pending, refresh, execute, clear, error, status } = useAsyncGraphqlQuery(
52
53
  String(queryName),
53
- params ?? {},
54
+ normalizedParams ?? {},
54
55
  asyncDataOptions
55
56
  );
56
57
  const transformError = ref(null);
@@ -115,25 +116,3 @@ export const useWPContent = (queryName, nodes, fixImagePaths, params, options) =
115
116
  isRetrying
116
117
  };
117
118
  };
118
- const transformData = (data, nodes, fixImagePaths) => {
119
- const transformedData = findData(data, nodes);
120
- if (fixImagePaths && transformedData && typeof transformedData === "object" && "featuredImage" in transformedData) {
121
- const featuredImage = transformedData.featuredImage;
122
- if (featuredImage && typeof featuredImage === "object" && "node" in featuredImage) {
123
- const node = featuredImage.node;
124
- if (node && typeof node === "object" && "sourceUrl" in node && typeof node.sourceUrl === "string") {
125
- node.relativePath = getRelativeImagePath(node.sourceUrl);
126
- }
127
- }
128
- }
129
- return transformedData;
130
- };
131
- const findData = (data, nodes) => {
132
- if (nodes.length === 0) return data;
133
- return nodes.reduce((acc, node) => {
134
- if (acc && typeof acc === "object" && node in acc) {
135
- return acc[node];
136
- }
137
- return void 0;
138
- }, data);
139
- };
@@ -4,23 +4,44 @@
4
4
  * Actual types are generated at runtime in consuming applications.
5
5
  */
6
6
 
7
- /* eslint-disable @typescript-eslint/no-explicit-any */
7
+ import type { Ref, ComputedRef, WatchSource, WatchCallback, WatchOptions } from 'vue'
8
+ import type { NuxtApp } from 'nuxt/app'
8
9
 
9
- // Stub for #imports
10
- export const computed: any
11
- export const ref: any
12
- export const watch: any
13
- export const defineNuxtPlugin: any
14
- export const useAsyncGraphqlQuery: any
15
- export const useGraphqlState: any
16
- export const useNuxtApp: any
17
- export const useRuntimeConfig: any
18
- export const useRoute: any
10
+ // Stub for #imports - Vue reactivity
11
+ export function computed<T>(getter: () => T): ComputedRef<T>
12
+ export function ref<T>(value: T): Ref<T>
13
+ export function watch<T>(
14
+ source: WatchSource<T> | WatchSource<T>[],
15
+ callback: WatchCallback<T>,
16
+ options?: WatchOptions
17
+ ): () => void
18
+
19
+ // Stub for #imports - Nuxt
20
+ export function defineNuxtPlugin(plugin: (nuxtApp: NuxtApp) => void | Promise<void>): unknown
21
+ export function useNuxtApp(): NuxtApp
22
+ export function useRuntimeConfig(): Record<string, unknown>
23
+ export function useRoute(): { path: string, params: Record<string, string>, query: Record<string, string> }
24
+
25
+ // Stub for #imports - nuxt-graphql-middleware
26
+ export function useAsyncGraphqlQuery<T = unknown>(
27
+ name: string,
28
+ params?: Record<string, unknown>,
29
+ options?: Record<string, unknown>
30
+ ): {
31
+ data: Ref<T | null>
32
+ pending: Ref<boolean>
33
+ refresh: () => Promise<void>
34
+ execute: () => Promise<void>
35
+ clear: () => void
36
+ error: Ref<Error | null>
37
+ status: Ref<string>
38
+ }
39
+ export function useGraphqlState(): Record<string, unknown>
19
40
 
20
41
  // Stub for #nuxt-graphql-middleware/operation-types
21
- export type Query = Record<string, any>
42
+ export type Query = Record<string, unknown>
22
43
 
23
- // Stub for #build/graphql-operations
24
- export type PostFragment = any
25
- export type PageFragment = any
26
- export type MenuItemFragment = any
44
+ // Stub for #build/graphql-operations - these are generated types
45
+ export type PostFragment = Record<string, unknown>
46
+ export type PageFragment = Record<string, unknown>
47
+ export type MenuItemFragment = Record<string, unknown>
File without changes
@@ -0,0 +1,33 @@
1
+ import { getRelativeImagePath } from "./images.js";
2
+ export const findData = (data, nodes) => {
3
+ if (nodes.length === 0) return data;
4
+ return nodes.reduce((acc, node) => {
5
+ if (acc && typeof acc === "object" && node in acc) {
6
+ return acc[node];
7
+ }
8
+ return void 0;
9
+ }, data);
10
+ };
11
+ export const transformData = (data, nodes, fixImagePaths) => {
12
+ const transformedData = findData(data, nodes);
13
+ if (fixImagePaths && transformedData && typeof transformedData === "object" && "featuredImage" in transformedData) {
14
+ const featuredImage = transformedData.featuredImage;
15
+ if (featuredImage && typeof featuredImage === "object" && "node" in featuredImage) {
16
+ const node = featuredImage.node;
17
+ if (node && typeof node === "object" && "sourceUrl" in node && typeof node.sourceUrl === "string") {
18
+ node.relativePath = getRelativeImagePath(node.sourceUrl);
19
+ }
20
+ }
21
+ }
22
+ return transformedData;
23
+ };
24
+ export const normalizeUriParam = (params) => {
25
+ if (!params || typeof params !== "object") return params;
26
+ const paramsObj = params;
27
+ if ("uri" in paramsObj && typeof paramsObj.uri === "string") {
28
+ const uri = paramsObj.uri;
29
+ const normalizedUri = uri.endsWith("/") ? uri : `${uri}/`;
30
+ return { ...paramsObj, uri: normalizedUri };
31
+ }
32
+ return params;
33
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpnuxt/core",
3
- "version": "2.0.0-alpha.7",
3
+ "version": "2.0.0-alpha.9",
4
4
  "description": "Nuxt module for WordPress integration via GraphQL (WPGraphQL)",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -43,8 +43,9 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@nuxt/kit": "4.2.2",
46
+ "@nuxt/kit": "4.3.0",
47
47
  "@radya/nuxt-dompurify": "^1.0.5",
48
+ "consola": "^3.4.0",
48
49
  "defu": "^6.1.4",
49
50
  "graphql": "^16.12.0",
50
51
  "nuxt-graphql-middleware": "5.3.2",
@@ -53,12 +54,12 @@
53
54
  "devDependencies": {
54
55
  "@nuxt/devtools": "^3.1.1",
55
56
  "@nuxt/module-builder": "^1.0.2",
56
- "@nuxt/schema": "4.2.2",
57
+ "@nuxt/schema": "4.3.0",
57
58
  "@nuxt/test-utils": "^3.23.0",
58
- "@types/node": "^25.0.9",
59
- "nuxt": "4.2.2",
60
- "vitest": "^4.0.17",
61
- "vue-tsc": "^3.2.2"
59
+ "@types/node": "^25.2.1",
60
+ "nuxt": "4.3.0",
61
+ "vitest": "^4.0.18",
62
+ "vue-tsc": "^3.2.3"
62
63
  },
63
64
  "peerDependencies": {
64
65
  "nuxt": "^4.0.0"