nuxtseo-shared 5.1.4 → 5.2.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.
@@ -3,11 +3,17 @@ import { BirpcGroup } from 'birpc';
3
3
  export { BirpcGroup } from 'birpc';
4
4
  import { Nuxt } from 'nuxt/schema';
5
5
 
6
+ /** Origin-root route the assembled (layer-mode) devtools client is served from. */
7
+ declare const UNIFIED_CLIENT_ROUTE = "/__nuxt-seo-devtools";
6
8
  interface DevToolsUIConfig {
7
- route: string;
9
+ /** Per-module route used by the legacy prebuilt-client mode. */
10
+ route?: string;
8
11
  name: string;
9
12
  title: string;
10
13
  icon: string;
14
+ /** Route segment inside the unified client (layer mode). Defaults to name minus `nuxt-`. */
15
+ slug?: string;
16
+ /** Legacy dev-proxy port (prebuilt-client mode only). */
11
17
  devPort?: number;
12
18
  }
13
19
  interface SeoModuleInfo {
@@ -16,8 +22,13 @@ interface SeoModuleInfo {
16
22
  icon: string;
17
23
  route: string;
18
24
  }
25
+ /**
26
+ * Register a module's devtools panel. Detects whether the module ships a source layer
27
+ * (current) or a prebuilt client (legacy) and handles each, so old and new modules can
28
+ * be mixed during migration.
29
+ */
19
30
  declare function setupDevToolsUI(config: DevToolsUIConfig, resolve: Resolver['resolve'], nuxt?: Nuxt): void;
20
31
  declare function setupDevToolsRpc<ServerFunctions extends object, ClientFunctions extends object>(namespace: string, serverFunctions: ServerFunctions, nuxt?: Nuxt): Promise<BirpcGroup<ClientFunctions, ServerFunctions>>;
21
32
 
22
- export { setupDevToolsRpc, setupDevToolsUI };
33
+ export { UNIFIED_CLIENT_ROUTE, setupDevToolsRpc, setupDevToolsUI };
23
34
  export type { DevToolsUIConfig, SeoModuleInfo };
@@ -3,11 +3,17 @@ import { BirpcGroup } from 'birpc';
3
3
  export { BirpcGroup } from 'birpc';
4
4
  import { Nuxt } from 'nuxt/schema';
5
5
 
6
+ /** Origin-root route the assembled (layer-mode) devtools client is served from. */
7
+ declare const UNIFIED_CLIENT_ROUTE = "/__nuxt-seo-devtools";
6
8
  interface DevToolsUIConfig {
7
- route: string;
9
+ /** Per-module route used by the legacy prebuilt-client mode. */
10
+ route?: string;
8
11
  name: string;
9
12
  title: string;
10
13
  icon: string;
14
+ /** Route segment inside the unified client (layer mode). Defaults to name minus `nuxt-`. */
15
+ slug?: string;
16
+ /** Legacy dev-proxy port (prebuilt-client mode only). */
11
17
  devPort?: number;
12
18
  }
13
19
  interface SeoModuleInfo {
@@ -16,8 +22,13 @@ interface SeoModuleInfo {
16
22
  icon: string;
17
23
  route: string;
18
24
  }
25
+ /**
26
+ * Register a module's devtools panel. Detects whether the module ships a source layer
27
+ * (current) or a prebuilt client (legacy) and handles each, so old and new modules can
28
+ * be mixed during migration.
29
+ */
19
30
  declare function setupDevToolsUI(config: DevToolsUIConfig, resolve: Resolver['resolve'], nuxt?: Nuxt): void;
20
31
  declare function setupDevToolsRpc<ServerFunctions extends object, ClientFunctions extends object>(namespace: string, serverFunctions: ServerFunctions, nuxt?: Nuxt): Promise<BirpcGroup<ClientFunctions, ServerFunctions>>;
21
32
 
22
- export { setupDevToolsRpc, setupDevToolsUI };
33
+ export { UNIFIED_CLIENT_ROUTE, setupDevToolsRpc, setupDevToolsUI };
23
34
  export type { DevToolsUIConfig, SeoModuleInfo };
package/dist/devtools.mjs CHANGED
@@ -1,38 +1,124 @@
1
- import { existsSync, readdirSync } from 'node:fs';
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
2
4
  import { onDevToolsInitialized, extendServerRpc, addCustomTab } from '@nuxt/devtools-kit';
3
5
  import { useNuxt } from '@nuxt/kit';
4
6
  import sirv from 'sirv';
5
7
 
6
- function setupDevToolsUI(config, resolve, nuxt = useNuxt()) {
7
- const { route, name, title, icon, devPort = 3030 } = config;
8
- const clientPath = resolve("./devtools");
8
+ const UNIFIED_CLIENT_ROUTE = "/__nuxt-seo-devtools";
9
+ const hashSet = (arr) => [...JSON.stringify(arr)].reduce((h, c) => h * 31 + c.charCodeAt(0) >>> 0, 7).toString(36);
10
+ function placeholderHtml() {
11
+ return `<!doctype html><html><head><meta charset="utf-8"><title>Nuxt SEO DevTools</title>
12
+ <style>html,body{margin:0;height:100%;font-family:'Hubot Sans',system-ui,sans-serif;background:oklch(98.4% 0.005 292);color:oklch(16% 0.036 292)}@media(prefers-color-scheme:dark){html,body{background:oklch(11% 0.029 292);color:oklch(96.8% 0.009 292)}}.wrap{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px}.spin{width:34px;height:34px;border-radius:50%;border:3px solid color-mix(in oklab,oklch(54% 0.225 292) 25%,transparent);border-top-color:oklch(54% 0.225 292);animation:s .8s linear infinite}@keyframes s{to{transform:rotate(360deg)}}h1{font-size:15px;font-weight:600;margin:0}p{font-size:13px;opacity:.6;margin:0}</style></head>
13
+ <body><div class="wrap"><div class="spin"></div><h1>Building Nuxt SEO DevTools\u2026</h1><p>Assembling panels for your installed modules. This runs once.</p></div>
14
+ <script>setInterval(async()=>{try{const r=await fetch('${UNIFIED_CLIENT_ROUTE}/__status');const j=await r.json();if(j.ready)location.reload()}catch{}},1000)<\/script></body></html>`;
15
+ }
16
+ function deriveRoutes(layerDir, slug) {
17
+ const routes = [`/${slug}`];
18
+ const pagesDir = join(layerDir, "pages", slug);
19
+ if (existsSync(pagesDir)) {
20
+ for (const f of readdirSync(pagesDir)) {
21
+ if (f.endsWith(".vue") && f !== "index.vue")
22
+ routes.push(`/${slug}/${f.slice(0, -4)}`);
23
+ }
24
+ }
25
+ return routes;
26
+ }
27
+ function registerSharedRpcOnce(nuxt) {
28
+ if (nuxt._seoDevtoolsRpcRegistered)
29
+ return;
30
+ nuxt._seoDevtoolsRpcRegistered = true;
31
+ onDevToolsInitialized(() => {
32
+ extendServerRpc("nuxt-seo-modules", {
33
+ getInstalledSeoModules: () => nuxt._seoDevtoolsModules || []
34
+ }, nuxt);
35
+ }, nuxt);
36
+ }
37
+ function generateAndBuild(cacheDir, installed, onReady) {
38
+ const routes = ["/", ...installed.flatMap((m) => deriveRoutes(m.layerDir, m.slug))];
39
+ const extendsList = ["nuxtseo-layer-devtools", ...installed.map((m) => m.layerDir)];
40
+ mkdirSync(join(cacheDir, "pages"), { recursive: true });
41
+ writeFileSync(join(cacheDir, "nuxt.config.ts"), `import { resolve } from 'pathe'
42
+ export default defineNuxtConfig({
43
+ extends: ${JSON.stringify(extendsList, null, 2)},
44
+ ssr: false,
45
+ robots: false,
46
+ content: false,
47
+ sitemap: false,
48
+ nitro: { prerender: { routes: ${JSON.stringify(routes)} }, output: { publicDir: resolve(__dirname, './dist/devtools') } },
49
+ app: { baseURL: '${UNIFIED_CLIENT_ROUTE}' },
50
+ compatibilityDate: '2026-03-13',
51
+ })
52
+ `);
53
+ writeFileSync(join(cacheDir, "app.vue"), `<template><NuxtPage /></template>
54
+ `);
55
+ writeFileSync(join(cacheDir, "pages/index.vue"), `<template><div class="p-4">${installed.map((m) => `<NuxtLink to="/${m.slug}" class="block underline">${m.title}</NuxtLink>`).join("")}</div></template>
56
+ `);
57
+ console.log(`[nuxt-seo] building devtools client for: ${installed.map((m) => m.slug).join(", ")}`);
58
+ const child = spawn("npx", ["nuxi", "build"], { cwd: cacheDir, stdio: "inherit" });
59
+ child.on("exit", (code) => {
60
+ if (code === 0) {
61
+ writeFileSync(join(cacheDir, ".installed-hash"), hashSet(installed.map((m) => m.slug).sort()));
62
+ onReady();
63
+ console.log("[nuxt-seo] devtools client ready");
64
+ }
65
+ });
66
+ }
67
+ function setupLayerModule(config, layerDir, nuxt) {
68
+ const slug = config.slug ?? config.name.replace(/^nuxt-/, "");
69
+ const clientRoute = `${UNIFIED_CLIENT_ROUTE}/${slug}`;
70
+ const modules = nuxt._seoDevtoolsModules ??= [];
71
+ modules.push({ name: config.name, title: config.title, icon: config.icon, route: clientRoute });
72
+ const layers = nuxt._seoDevtoolsLayers ??= [];
73
+ layers.push({ slug, name: config.name, title: config.title, icon: config.icon, layerDir });
74
+ addCustomTab({ name: `nuxt-seo-${slug}`, title: config.title, icon: config.icon, view: { type: "iframe", src: clientRoute } });
75
+ if (nuxt._seoDevtoolsInit)
76
+ return;
77
+ nuxt._seoDevtoolsInit = true;
78
+ const cacheDir = join(nuxt.options.rootDir, "node_modules/.cache/nuxt-seo-devtools");
79
+ const dist = join(cacheDir, "dist/devtools");
80
+ const state = { ready: false };
81
+ nuxt.hook("modules:done", () => {
82
+ const installed = nuxt._seoDevtoolsLayers;
83
+ const key = hashSet(installed.map((m) => m.slug).sort());
84
+ state.ready = existsSync(join(cacheDir, ".installed-hash")) && readFileSync(join(cacheDir, ".installed-hash"), "utf8") === key && existsSync(dist);
85
+ if (!state.ready)
86
+ generateAndBuild(cacheDir, installed, () => {
87
+ state.ready = true;
88
+ });
89
+ });
90
+ nuxt.hook("vite:serverCreated", (server) => {
91
+ const serve = sirv(dist, { dev: true, single: "200.html" });
92
+ server.middlewares.use(UNIFIED_CLIENT_ROUTE, (req, res, next) => {
93
+ if ((req.url || "/").startsWith("/__status")) {
94
+ res.setHeader("content-type", "application/json");
95
+ return res.end(JSON.stringify({ ready: state.ready }));
96
+ }
97
+ if (!state.ready) {
98
+ res.setHeader("content-type", "text/html");
99
+ return res.end(placeholderHtml());
100
+ }
101
+ return serve(req, res, next);
102
+ });
103
+ });
104
+ }
105
+ function setupLegacyModule(config, clientPath, nuxt) {
106
+ const { name, title, icon, devPort = 3030 } = config;
107
+ const route = config.route ?? `/__${name.replace(/^nuxt-/, "")}`;
9
108
  const modules = nuxt._seoDevtoolsModules ??= [];
10
109
  modules.push({ name, title, icon, route });
11
- if (!nuxt._seoDevtoolsRpcRegistered) {
12
- nuxt._seoDevtoolsRpcRegistered = true;
13
- onDevToolsInitialized(() => {
14
- extendServerRpc("nuxt-seo-modules", {
15
- getInstalledSeoModules() {
16
- return nuxt._seoDevtoolsModules || [];
17
- }
18
- }, nuxt);
19
- }, nuxt);
20
- }
21
110
  const isProductionBuild = existsSync(clientPath) && readdirSync(clientPath).length > 0;
22
111
  if (isProductionBuild) {
23
112
  nuxt.hook("vite:serverCreated", (server) => {
24
- server.middlewares.use(
25
- route,
26
- sirv(clientPath, { dev: true, single: true })
27
- );
113
+ server.middlewares.use(route, sirv(clientPath, { dev: true, single: true }));
28
114
  });
29
115
  } else {
30
- nuxt.hook("vite:extendConfig", (config2) => {
31
- Object.assign(config2, {
116
+ nuxt.hook("vite:extendConfig", (viteConfig) => {
117
+ Object.assign(viteConfig, {
32
118
  server: {
33
- ...config2.server,
119
+ ...viteConfig.server,
34
120
  proxy: {
35
- ...config2.server?.proxy,
121
+ ...viteConfig.server?.proxy,
36
122
  [route]: {
37
123
  target: `http://localhost:${devPort}${route}`,
38
124
  changeOrigin: true,
@@ -53,15 +139,18 @@ function setupDevToolsUI(config, resolve, nuxt = useNuxt()) {
53
139
  });
54
140
  });
55
141
  }
56
- addCustomTab({
57
- name,
58
- title,
59
- icon,
60
- view: {
61
- type: "iframe",
62
- src: route
63
- }
64
- });
142
+ addCustomTab({ name, title, icon, view: { type: "iframe", src: route } });
143
+ }
144
+ function setupDevToolsUI(config, resolve, nuxt = useNuxt()) {
145
+ if (!nuxt.options.dev)
146
+ return;
147
+ const layerDir = resolve("./devtools");
148
+ const isLayer = existsSync(join(layerDir, "nuxt.config.ts")) && !existsSync(join(layerDir, "index.html"));
149
+ registerSharedRpcOnce(nuxt);
150
+ if (isLayer)
151
+ setupLayerModule(config, layerDir, nuxt);
152
+ else
153
+ setupLegacyModule(config, layerDir, nuxt);
65
154
  }
66
155
  function setupDevToolsRpc(namespace, serverFunctions, nuxt = useNuxt()) {
67
156
  return new Promise((resolve) => {
@@ -71,4 +160,4 @@ function setupDevToolsRpc(namespace, serverFunctions, nuxt = useNuxt()) {
71
160
  });
72
161
  }
73
162
 
74
- export { setupDevToolsRpc, setupDevToolsUI };
163
+ export { UNIFIED_CLIENT_ROUTE, setupDevToolsRpc, setupDevToolsUI };
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "5.1.4",
7
+ "version": "5.2.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxtseo-shared",
3
3
  "type": "module",
4
- "version": "5.1.4",
4
+ "version": "5.2.0",
5
5
  "description": "Shared utilities for Nuxt SEO modules.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",