nuxtseo-shared 5.2.6 → 5.3.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.
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)];
@@ -169,7 +230,8 @@ function setupLayerModule(config, layerDir, nuxt) {
169
230
  nuxt._seoDevtoolsInit = true;
170
231
  const cacheDir = join(nuxt.options.rootDir, "node_modules/.cache/nuxt-seo-devtools");
171
232
  const dist = join(cacheDir, "dist/devtools");
172
- const state = { ready: false, building: false, failed: false, startedAt: 0, step: "", modules: [] };
233
+ const state = { ready: false, building: false, installing: false, needsInstall: false, failed: false, startedAt: 0, step: "", modules: [] };
234
+ const rootDir = nuxt.options.rootDir;
173
235
  function ensureBuilt() {
174
236
  if (state.ready || state.building)
175
237
  return;
@@ -179,7 +241,7 @@ function setupLayerModule(config, layerDir, nuxt) {
179
241
  state.step = "Starting build\u2026";
180
242
  const installed = nuxt._seoDevtoolsLayers;
181
243
  state.modules = installed.map((m) => m.title);
182
- generateAndBuild(cacheDir, nuxt.options.rootDir, installed, {
244
+ generateAndBuild(cacheDir, rootDir, installed, {
183
245
  onProgress: (step) => {
184
246
  state.step = step;
185
247
  },
@@ -187,13 +249,38 @@ function setupLayerModule(config, layerDir, nuxt) {
187
249
  state.ready = true;
188
250
  state.building = false;
189
251
  },
190
- onError: () => {
252
+ onError: (message) => {
191
253
  state.building = false;
192
254
  state.failed = true;
193
- state.step = "Build failed, see the dev server logs";
255
+ state.step = message ?? "Build failed, see the dev server logs";
194
256
  }
195
257
  });
196
258
  }
259
+ function startInstall() {
260
+ if (state.installing || state.building || state.ready)
261
+ return;
262
+ state.installing = true;
263
+ state.needsInstall = false;
264
+ state.failed = false;
265
+ state.startedAt = Date.now();
266
+ state.step = `Installing ${TOOLCHAIN_PACKAGE}\u2026`;
267
+ installToolchain(rootDir, nuxt, (step) => {
268
+ state.step = step;
269
+ }).then((ok) => {
270
+ state.installing = false;
271
+ if (ok) {
272
+ ensureBuilt();
273
+ } else {
274
+ state.failed = true;
275
+ state.step = `Could not install ${TOOLCHAIN_PACKAGE} \u2014 see the dev server logs`;
276
+ }
277
+ }).catch((err) => {
278
+ state.installing = false;
279
+ state.failed = true;
280
+ state.step = `Could not install ${TOOLCHAIN_PACKAGE} \u2014 see the dev server logs`;
281
+ console.error(`[nuxt-seo] devtools toolchain install failed: ${err.message}`);
282
+ });
283
+ }
197
284
  nuxt.hook("modules:done", () => {
198
285
  const installed = nuxt._seoDevtoolsLayers;
199
286
  const key = hashSet(installed.map((m) => m.slug).sort());
@@ -202,19 +289,32 @@ function setupLayerModule(config, layerDir, nuxt) {
202
289
  nuxt.hook("vite:serverCreated", (server) => {
203
290
  const serve = sirv(dist, { dev: true, single: "200.html" });
204
291
  server.middlewares.use(UNIFIED_CLIENT_ROUTE, (req, res, next) => {
205
- if ((req.url || "/").startsWith("/__status")) {
206
- ensureBuilt();
292
+ const url = req.url || "/";
293
+ if (url.startsWith("/__install")) {
294
+ startInstall();
295
+ res.setHeader("content-type", "application/json");
296
+ return res.end(JSON.stringify({ installing: state.installing }));
297
+ }
298
+ if (url.startsWith("/__status")) {
299
+ if (!state.ready && !state.building && !state.installing) {
300
+ if (toolchainInstalled(rootDir))
301
+ ensureBuilt();
302
+ else
303
+ state.needsInstall = true;
304
+ }
207
305
  res.setHeader("content-type", "application/json");
208
306
  return res.end(JSON.stringify({
209
307
  ready: state.ready,
210
308
  failed: state.failed,
309
+ needsInstall: state.needsInstall,
310
+ installing: state.installing,
311
+ packageName: TOOLCHAIN_PACKAGE,
211
312
  step: state.step,
212
313
  modules: state.modules,
213
314
  elapsed: state.startedAt ? Date.now() - state.startedAt : 0
214
315
  }));
215
316
  }
216
317
  if (!state.ready) {
217
- ensureBuilt();
218
318
  res.setHeader("content-type", "text/html");
219
319
  return res.end(placeholderHtml());
220
320
  }
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.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.2.6",
4
+ "version": "5.3.0",
5
5
  "description": "Shared utilities for Nuxt SEO modules.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -81,6 +81,7 @@
81
81
  "birpc": "^4.0.0",
82
82
  "consola": "^3.4.2",
83
83
  "defu": "^6.1.7",
84
+ "nypm": "^0.6.6",
84
85
  "ofetch": "^1.5.1",
85
86
  "pathe": "^2.0.3",
86
87
  "pkg-types": "^2.3.1",
@@ -96,7 +97,7 @@
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
102
  "vitest": "^4.1.8",
102
103
  "vue": "^3.5.35",