nuxtseo-shared 5.2.6 → 5.3.1

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/devtools.mjs CHANGED
@@ -3,8 +3,9 @@ import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from
3
3
  import { createRequire } from 'node:module';
4
4
  import { join, dirname } from 'node:path';
5
5
  import process from 'node:process';
6
- import { onDevToolsInitialized, extendServerRpc, addCustomTab } from '@nuxt/devtools-kit';
6
+ import { onDevToolsInitialized, extendServerRpc, addCustomTab, startSubprocess } from '@nuxt/devtools-kit';
7
7
  import { useNuxt } from '@nuxt/kit';
8
+ import { detectPackageManager } from 'nypm';
8
9
  import sirv from 'sirv';
9
10
  import { modules } from './const.mjs';
10
11
  import { detectNuxtSeoModules } from './kit.mjs';
@@ -17,22 +18,44 @@ function npmForSlug(slug) {
17
18
  return modules.find((m) => m.slug === slug)?.npm;
18
19
  }
19
20
  const UNIFIED_CLIENT_ROUTE = "/__nuxt-seo-devtools";
21
+ const TOOLCHAIN_PACKAGE = "nuxtseo-layer-devtools";
20
22
  const hashSet = (arr) => [...JSON.stringify(arr)].reduce((h, c) => h * 31 + c.charCodeAt(0) >>> 0, 7).toString(36);
21
23
  function placeholderHtml() {
22
24
  return `<!doctype html><html><head><meta charset="utf-8"><title>Nuxt SEO DevTools</title>
23
- <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:14px;padding:24px;text-align:center}.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}.mods{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;max-width:420px}.chip{font-size:11px;padding:2px 9px;border-radius:999px;background:color-mix(in oklab,oklch(54% 0.225 292) 14%,transparent);color:oklch(54% 0.225 292)}.step{font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;opacity:.55;max-width:520px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:16px}.t{font-variant-numeric:tabular-nums;opacity:.85}.err .spin{border-top-color:oklch(60% 0.2 25);border-color:color-mix(in oklab,oklch(60% 0.2 25) 25%,transparent);animation:none}</style></head>
24
- <body><div class="wrap" id="wrap"><div class="spin"></div><h1 id="title">Building Nuxt SEO DevTools\u2026</h1><div class="mods" id="mods"></div><p>Assembling panels for your installed modules. This runs once.</p><div class="step" id="step"></div></div>
25
+ <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:14px;padding:24px;text-align:center}.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;max-width:440px}.mods{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;max-width:420px}.chip{font-size:11px;padding:2px 9px;border-radius:999px;background:color-mix(in oklab,oklch(54% 0.225 292) 14%,transparent);color:oklch(54% 0.225 292)}.btn{font:inherit;font-size:13px;font-weight:600;padding:7px 16px;border-radius:8px;border:0;background:oklch(54% 0.225 292);color:#fff;cursor:pointer}.btn:hover{background:oklch(49% 0.225 292)}.btn:disabled{opacity:.6;cursor:default}.hide{display:none}.step{font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;opacity:.55;max-width:520px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:16px}.t{font-variant-numeric:tabular-nums;opacity:.85}.err .spin{border-top-color:oklch(60% 0.2 25);border-color:color-mix(in oklab,oklch(60% 0.2 25) 25%,transparent);animation:none}</style></head>
26
+ <body><div class="wrap" id="wrap"><div class="spin" id="spin"></div><h1 id="title">Nuxt SEO DevTools</h1><div class="mods" id="mods"></div><p id="desc">Starting\u2026</p><button class="btn hide" id="install">Install</button><div class="step" id="step"></div></div>
25
27
  <script>
26
28
  const $=id=>document.getElementById(id)
27
- let mods=false
29
+ const esc=s=>(s||'').replace(/[<>&]/g,c=>({'<':'&lt;','>':'&gt;','&':'&amp;'}[c]))
30
+ let mods=false,clicked=false
31
+ $('install').addEventListener('click',async()=>{
32
+ clicked=true
33
+ $('install').disabled=true;$('install').classList.add('hide')
34
+ $('spin').classList.remove('hide')
35
+ $('title').textContent='Installing\u2026'
36
+ $('desc').textContent='Adding the dev-only DevTools UI to your project.'
37
+ try{await fetch('${UNIFIED_CLIENT_ROUTE}/__install')}catch{}
38
+ })
28
39
  async function poll(){
29
40
  try{
30
41
  const j=await (await fetch('${UNIFIED_CLIENT_ROUTE}/__status')).json()
31
42
  if(j.ready){location.reload();return}
32
- if(!mods&&Array.isArray(j.modules)&&j.modules.length){mods=true;$('mods').innerHTML=j.modules.map(m=>'<span class="chip">'+m+'</span>').join('')}
33
- if(j.failed){$('wrap').classList.add('err');$('title').textContent='DevTools build failed';$('step').textContent=j.step||'';return}
43
+ if(!mods&&Array.isArray(j.modules)&&j.modules.length){mods=true;$('mods').innerHTML=j.modules.map(m=>'<span class="chip">'+esc(m)+'</span>').join('')}
44
+ if(j.failed){$('wrap').classList.add('err');$('spin').classList.remove('hide');$('install').classList.add('hide');$('title').textContent='DevTools unavailable';$('desc').textContent='';$('step').textContent=j.step||'';return}
45
+ if(j.needsInstall&&!clicked){
46
+ const pkg=j.packageName||'nuxtseo-devtools'
47
+ $('spin').classList.add('hide')
48
+ $('install').classList.remove('hide');$('install').disabled=false;$('install').textContent='Install '+pkg
49
+ $('title').textContent='Nuxt SEO DevTools'
50
+ $('desc').textContent='The panel needs '+pkg+' (dev only). Installing it adds the package to your devDependencies; nothing is changed until you choose to install.'
51
+ $('step').textContent=''
52
+ return
53
+ }
54
+ $('spin').classList.remove('hide');$('install').classList.add('hide')
55
+ $('title').textContent=j.installing?'Installing\u2026':'Building Nuxt SEO DevTools\u2026'
56
+ $('desc').textContent=j.installing?'Adding the dev-only DevTools UI to your project.':'Assembling panels for your installed modules. This runs once.'
34
57
  const t=j.elapsed?' \xB7 <span class="t">'+Math.round(j.elapsed/1000)+'s</span>':''
35
- $('step').innerHTML=(j.step?j.step.replace(/[<>&]/g,c=>({'<':'&lt;','>':'&gt;','&':'&amp;'}[c])):'')+t
58
+ $('step').innerHTML=esc(j.step)+t
36
59
  }catch{}
37
60
  }
38
61
  setInterval(poll,800);poll()
@@ -109,6 +132,44 @@ function resolveNuxtCli(rootDir) {
109
132
  return null;
110
133
  }
111
134
  const stripAnsi = (s) => s.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"), "");
135
+ function toolchainInstalled(rootDir) {
136
+ try {
137
+ createRequire(join(rootDir, "index.js")).resolve(TOOLCHAIN_PACKAGE);
138
+ return true;
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+ async function devAddArgs(rootDir) {
144
+ const pm = await detectPackageManager(rootDir).catch(() => null);
145
+ const name = pm?.name ?? "npm";
146
+ if (name === "npm")
147
+ return { command: "npm", args: ["install", "--save-dev", TOOLCHAIN_PACKAGE] };
148
+ if (name === "yarn")
149
+ return { command: "yarn", args: ["add", "--dev", TOOLCHAIN_PACKAGE] };
150
+ return { command: name, args: ["add", "-D", TOOLCHAIN_PACKAGE] };
151
+ }
152
+ async function installToolchain(rootDir, nuxt, onProgress) {
153
+ const { command, args } = await devAddArgs(rootDir);
154
+ onProgress(`Installing ${TOOLCHAIN_PACKAGE} with ${command}\u2026`);
155
+ return await new Promise((resolve) => {
156
+ const { getProcess } = startSubprocess(
157
+ { command, args, cwd: rootDir },
158
+ { id: "nuxt-seo:install-devtools", name: `Install ${TOOLCHAIN_PACKAGE}`, icon: "carbon:download" },
159
+ nuxt
160
+ );
161
+ const proc = getProcess();
162
+ if (!proc) {
163
+ resolve(false);
164
+ return;
165
+ }
166
+ proc.on("exit", (code) => resolve(code === 0 && toolchainInstalled(rootDir)));
167
+ proc.on("error", (err) => {
168
+ console.error(`[nuxt-seo] could not run "${command} ${args.join(" ")}": ${err.message}`);
169
+ resolve(false);
170
+ });
171
+ });
172
+ }
112
173
  function generateAndBuild(cacheDir, rootDir, installed, hooks) {
113
174
  const routes = ["/", ...installed.flatMap((m) => deriveRoutes(m.layerDir, m.slug))];
114
175
  const extendsList = [resolveBaseLayer(installed), ...installed.map((m) => m.layerDir)];
@@ -133,24 +194,38 @@ export default defineNuxtConfig({
133
194
  const cliBin = resolveNuxtCli(rootDir);
134
195
  const child = cliBin ? spawn(process.execPath, [cliBin, "build"], { cwd: cacheDir, stdio: ["inherit", "pipe", "pipe"] }) : spawn("npx", ["nuxi", "build"], { cwd: cacheDir, stdio: ["inherit", "pipe", "pipe"], shell: true });
135
196
  const statusLine = /^(?:[ℹ✔✓✨⚠✖✗●➜√]|\[\w)/;
136
- const forward = (chunk, sink) => {
137
- sink.write(chunk);
197
+ const buffered = [];
198
+ let drawing = false;
199
+ const capture = (chunk) => {
200
+ buffered.push(chunk);
138
201
  const step = stripAnsi(chunk.toString()).split("\n").map((l) => l.trim()).filter((l) => statusLine.test(l)).pop();
139
- if (step)
202
+ if (step) {
140
203
  hooks.onProgress(step);
204
+ process.stdout.write(`\r\x1B[2K[nuxt-seo] ${step}`);
205
+ drawing = true;
206
+ }
207
+ };
208
+ const endLine = () => {
209
+ if (drawing) {
210
+ process.stdout.write("\n");
211
+ drawing = false;
212
+ }
141
213
  };
142
- child.stdout?.on("data", (c) => forward(c, process.stdout));
143
- child.stderr?.on("data", (c) => forward(c, process.stderr));
214
+ child.stdout?.on("data", capture);
215
+ child.stderr?.on("data", capture);
144
216
  child.on("error", (err) => {
217
+ endLine();
145
218
  console.error(`[nuxt-seo] could not build devtools client, the panel will stay unavailable: ${err.message}`);
146
219
  hooks.onError();
147
220
  });
148
221
  child.on("exit", (code) => {
222
+ endLine();
149
223
  if (code === 0) {
150
224
  writeFileSync(join(cacheDir, ".installed-hash"), hashSet(installed.map((m) => m.slug).sort()));
151
225
  hooks.onReady();
152
226
  console.log("[nuxt-seo] devtools client ready");
153
227
  } else {
228
+ process.stderr.write(Buffer.concat(buffered));
154
229
  console.error(`[nuxt-seo] devtools client build exited with code ${code}, the panel will stay unavailable`);
155
230
  hooks.onError();
156
231
  }
@@ -169,7 +244,8 @@ function setupLayerModule(config, layerDir, nuxt) {
169
244
  nuxt._seoDevtoolsInit = true;
170
245
  const cacheDir = join(nuxt.options.rootDir, "node_modules/.cache/nuxt-seo-devtools");
171
246
  const dist = join(cacheDir, "dist/devtools");
172
- const state = { ready: false, building: false, failed: false, startedAt: 0, step: "", modules: [] };
247
+ const state = { ready: false, building: false, installing: false, needsInstall: false, failed: false, startedAt: 0, step: "", modules: [] };
248
+ const rootDir = nuxt.options.rootDir;
173
249
  function ensureBuilt() {
174
250
  if (state.ready || state.building)
175
251
  return;
@@ -179,7 +255,7 @@ function setupLayerModule(config, layerDir, nuxt) {
179
255
  state.step = "Starting build\u2026";
180
256
  const installed = nuxt._seoDevtoolsLayers;
181
257
  state.modules = installed.map((m) => m.title);
182
- generateAndBuild(cacheDir, nuxt.options.rootDir, installed, {
258
+ generateAndBuild(cacheDir, rootDir, installed, {
183
259
  onProgress: (step) => {
184
260
  state.step = step;
185
261
  },
@@ -187,13 +263,38 @@ function setupLayerModule(config, layerDir, nuxt) {
187
263
  state.ready = true;
188
264
  state.building = false;
189
265
  },
190
- onError: () => {
266
+ onError: (message) => {
191
267
  state.building = false;
192
268
  state.failed = true;
193
- state.step = "Build failed, see the dev server logs";
269
+ state.step = message ?? "Build failed, see the dev server logs";
194
270
  }
195
271
  });
196
272
  }
273
+ function startInstall() {
274
+ if (state.installing || state.building || state.ready)
275
+ return;
276
+ state.installing = true;
277
+ state.needsInstall = false;
278
+ state.failed = false;
279
+ state.startedAt = Date.now();
280
+ state.step = `Installing ${TOOLCHAIN_PACKAGE}\u2026`;
281
+ installToolchain(rootDir, nuxt, (step) => {
282
+ state.step = step;
283
+ }).then((ok) => {
284
+ state.installing = false;
285
+ if (ok) {
286
+ ensureBuilt();
287
+ } else {
288
+ state.failed = true;
289
+ state.step = `Could not install ${TOOLCHAIN_PACKAGE} \u2014 see the dev server logs`;
290
+ }
291
+ }).catch((err) => {
292
+ state.installing = false;
293
+ state.failed = true;
294
+ state.step = `Could not install ${TOOLCHAIN_PACKAGE} \u2014 see the dev server logs`;
295
+ console.error(`[nuxt-seo] devtools toolchain install failed: ${err.message}`);
296
+ });
297
+ }
197
298
  nuxt.hook("modules:done", () => {
198
299
  const installed = nuxt._seoDevtoolsLayers;
199
300
  const key = hashSet(installed.map((m) => m.slug).sort());
@@ -202,19 +303,32 @@ function setupLayerModule(config, layerDir, nuxt) {
202
303
  nuxt.hook("vite:serverCreated", (server) => {
203
304
  const serve = sirv(dist, { dev: true, single: "200.html" });
204
305
  server.middlewares.use(UNIFIED_CLIENT_ROUTE, (req, res, next) => {
205
- if ((req.url || "/").startsWith("/__status")) {
206
- ensureBuilt();
306
+ const url = req.url || "/";
307
+ if (url.startsWith("/__install")) {
308
+ startInstall();
309
+ res.setHeader("content-type", "application/json");
310
+ return res.end(JSON.stringify({ installing: state.installing }));
311
+ }
312
+ if (url.startsWith("/__status")) {
313
+ if (!state.ready && !state.building && !state.installing) {
314
+ if (toolchainInstalled(rootDir))
315
+ ensureBuilt();
316
+ else
317
+ state.needsInstall = true;
318
+ }
207
319
  res.setHeader("content-type", "application/json");
208
320
  return res.end(JSON.stringify({
209
321
  ready: state.ready,
210
322
  failed: state.failed,
323
+ needsInstall: state.needsInstall,
324
+ installing: state.installing,
325
+ packageName: TOOLCHAIN_PACKAGE,
211
326
  step: state.step,
212
327
  modules: state.modules,
213
328
  elapsed: state.startedAt ? Date.now() - state.startedAt : 0
214
329
  }));
215
330
  }
216
331
  if (!state.ready) {
217
- ensureBuilt();
218
332
  res.setHeader("content-type", "text/html");
219
333
  return res.end(placeholderHtml());
220
334
  }
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "5.2.6",
7
+ "version": "5.3.1",
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.2.6",
4
+ "version": "5.3.1",
5
5
  "description": "Shared utilities for Nuxt SEO modules.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -63,7 +63,7 @@
63
63
  "@nuxt/schema": "^3.16.0 || ^4.0.0",
64
64
  "nuxt": "^3.16.0 || ^4.0.0",
65
65
  "nuxt-site-config": "^3.2.0 || ^4.0.0",
66
- "vue": "^3.5.0",
66
+ "vue": "^3.5.38",
67
67
  "zod": "^3.23.0 || ^4.0.0"
68
68
  },
69
69
  "peerDependenciesMeta": {
@@ -75,12 +75,13 @@
75
75
  }
76
76
  },
77
77
  "dependencies": {
78
- "@clack/prompts": "^1.5.1",
78
+ "@clack/prompts": "^1.6.0",
79
79
  "@nuxt/devtools-kit": "4.0.0-alpha.3",
80
80
  "@nuxt/kit": "^4.4.8",
81
81
  "birpc": "^4.0.0",
82
82
  "consola": "^3.4.2",
83
83
  "defu": "^6.1.7",
84
+ "nypm": "^0.6.7",
84
85
  "ofetch": "^1.5.1",
85
86
  "pathe": "^2.0.3",
86
87
  "pkg-types": "^2.3.1",
@@ -90,17 +91,17 @@
90
91
  "ufo": "^1.6.4"
91
92
  },
92
93
  "devDependencies": {
93
- "@arethetypeswrong/cli": "^0.18.3",
94
+ "@arethetypeswrong/cli": "^0.18.4",
94
95
  "@nuxt/module-builder": "^1.0.2",
95
96
  "@nuxt/schema": "^4.4.8",
96
97
  "@nuxtjs/i18n": "^10.4.0",
97
98
  "nitropack": "^2.13.4",
98
99
  "nuxt": "^4.4.8",
99
- "nuxt-site-config": "^4.0.8",
100
+ "nuxt-site-config": "^4.1.0",
100
101
  "typescript": "^6.0.3",
101
- "vitest": "^4.1.8",
102
- "vue": "^3.5.35",
103
- "vue-tsc": "^3.3.4"
102
+ "vitest": "^4.1.9",
103
+ "vue": "^3.5.38",
104
+ "vue-tsc": "^3.3.5"
104
105
  },
105
106
  "scripts": {
106
107
  "build": "nuxt-module-build build",