@riebeckite/honox 0.0.31 → 0.0.32

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/index.js CHANGED
@@ -1167,7 +1167,7 @@ function isOutputCacheState(value) {
1167
1167
  }
1168
1168
  function isOutputInventory(value) {
1169
1169
  return typeof value === "object" && value !== null && !Array.isArray(value) && Object.entries(value).every(
1170
- ([outputPath, checksum]) => isSafeLogicalOutputPath(outputPath) && isChecksum(checksum)
1170
+ ([outputPath2, checksum]) => isSafeLogicalOutputPath(outputPath2) && isChecksum(checksum)
1171
1171
  );
1172
1172
  }
1173
1173
  function isOutputCacheEntries(value) {
@@ -1175,7 +1175,7 @@ function isOutputCacheEntries(value) {
1175
1175
  return false;
1176
1176
  }
1177
1177
  return Object.entries(value).every(
1178
- ([outputPath, entry]) => isSafeLogicalOutputPath(outputPath) && typeof entry === "object" && entry !== null && typeof entry.cachePath === "string" && entry.cachePath === outputCacheEntryName(outputPath) && isChecksum(entry.checksum)
1178
+ ([outputPath2, entry]) => isSafeLogicalOutputPath(outputPath2) && typeof entry === "object" && entry !== null && typeof entry.cachePath === "string" && entry.cachePath === outputCacheEntryName(outputPath2) && isChecksum(entry.checksum)
1179
1179
  );
1180
1180
  }
1181
1181
  function outputCacheEntriesPath(outputCachePath) {
@@ -1184,31 +1184,31 @@ function outputCacheEntriesPath(outputCachePath) {
1184
1184
  async function saveOutputCacheEntries(directory, outputDirectory, outputs, finalOutputInventory, timingsMs) {
1185
1185
  await ensureOutputCacheEntriesDirectory(directory);
1186
1186
  const entries = {};
1187
- for (const outputPath of Object.keys(outputs).sort()) {
1188
- assertSafeLogicalOutputPath(outputPath);
1189
- const existing = outputs[outputPath];
1187
+ for (const outputPath2 of Object.keys(outputs).sort()) {
1188
+ assertSafeLogicalOutputPath(outputPath2);
1189
+ const existing = outputs[outputPath2];
1190
1190
  if (existing.cachePath && existing.checksum) {
1191
- if (existing.cachePath !== outputCacheEntryName(outputPath) || !isChecksum(existing.checksum)) {
1192
- throw new Error(`Invalid SSG output cache entry: ${outputPath}`);
1191
+ if (existing.cachePath !== outputCacheEntryName(outputPath2) || !isChecksum(existing.checksum)) {
1192
+ throw new Error(`Invalid SSG output cache entry: ${outputPath2}`);
1193
1193
  }
1194
- entries[outputPath] = existing;
1194
+ entries[outputPath2] = existing;
1195
1195
  continue;
1196
1196
  }
1197
- const source = resolveOutputCacheFile(outputDirectory, outputPath);
1198
- const cachePath = outputCacheEntryName(outputPath);
1197
+ const source = resolveOutputCacheFile(outputDirectory, outputPath2);
1198
+ const cachePath = outputCacheEntryName(outputPath2);
1199
1199
  const destination = join3(directory, cachePath);
1200
1200
  await waitForFaultInjection("entry-create-start");
1201
1201
  await replaceOutputCacheEntry(source, destination, timingsMs);
1202
- const checksum = finalOutputInventory?.[outputPath];
1202
+ const checksum = finalOutputInventory?.[outputPath2];
1203
1203
  if (!checksum) {
1204
1204
  const cachedChecksum = await checksumFile(destination);
1205
1205
  if (!cachedChecksum) {
1206
- throw new Error(`Could not checksum cached output: ${outputPath}`);
1206
+ throw new Error(`Could not checksum cached output: ${outputPath2}`);
1207
1207
  }
1208
- entries[outputPath] = { cachePath, checksum: cachedChecksum };
1208
+ entries[outputPath2] = { cachePath, checksum: cachedChecksum };
1209
1209
  continue;
1210
1210
  }
1211
- entries[outputPath] = { cachePath, checksum };
1211
+ entries[outputPath2] = { cachePath, checksum };
1212
1212
  }
1213
1213
  return entries;
1214
1214
  }
@@ -1216,8 +1216,8 @@ async function validateOutputCacheEntries(outputCachePath, entries, timingsMs) {
1216
1216
  try {
1217
1217
  const directory = outputCacheEntriesPath(outputCachePath);
1218
1218
  if (!await isSafeOutputCacheEntriesDirectory(directory)) return false;
1219
- for (const [outputPath, entry] of Object.entries(entries)) {
1220
- if (!isSafeLogicalOutputPath(outputPath) || entry.cachePath !== outputCacheEntryName(outputPath) || !isChecksum(entry.checksum)) {
1219
+ for (const [outputPath2, entry] of Object.entries(entries)) {
1220
+ if (!isSafeLogicalOutputPath(outputPath2) || entry.cachePath !== outputCacheEntryName(outputPath2) || !isChecksum(entry.checksum)) {
1221
1221
  return false;
1222
1222
  }
1223
1223
  const path11 = resolveOutputCacheFile(directory, entry.cachePath);
@@ -1358,8 +1358,8 @@ function assertSafeLogicalOutputPath(path11) {
1358
1358
  throw new Error(`Invalid SSG output path: ${path11}`);
1359
1359
  }
1360
1360
  }
1361
- function outputCacheEntryName(outputPath) {
1362
- return createHash3("sha256").update(outputPath).digest("hex");
1361
+ function outputCacheEntryName(outputPath2) {
1362
+ return createHash3("sha256").update(outputPath2).digest("hex");
1363
1363
  }
1364
1364
  function isChecksum(value) {
1365
1365
  return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
@@ -1595,7 +1595,7 @@ function riebeckiteSsg(options = {}) {
1595
1595
  );
1596
1596
  viteClientAssetPaths = [
1597
1597
  .../* @__PURE__ */ new Set([...viteClientAssetPaths, ...Object.keys(bundle)])
1598
- ].filter((outputPath) => !siteOwnedOutputPaths.has(outputPath)).sort();
1598
+ ].filter((outputPath2) => !siteOwnedOutputPaths.has(outputPath2)).sort();
1599
1599
  const siteOutputCollisions = findBuildOutputSiteCollisions(
1600
1600
  Object.keys(bundle),
1601
1601
  siteOwnedOutputPaths
@@ -2091,12 +2091,18 @@ function riebeckiteClientModule(getConfig) {
2091
2091
  }
2092
2092
  function createClientModule(config, development) {
2093
2093
  const imports = [];
2094
- const initializers = [];
2094
+ const appInitializers = [];
2095
+ const pageInitializers = [];
2096
+ imports.push('import { ClientLifecycle } from "@riebeckite/core/client";');
2097
+ if (config.spa !== void 0) {
2098
+ imports.push('import { createClient } from "honox/client";');
2099
+ imports.push('import { startSpaNavigation } from "@riebeckite/spa";');
2100
+ }
2095
2101
  if (development) {
2096
2102
  imports.push(
2097
2103
  'import { initDevelopmentInspector } from "virtual:riebeckite/development-inspector";'
2098
2104
  );
2099
- initializers.push("initDevelopmentInspector()");
2105
+ appInitializers.push("initDevelopmentInspector()");
2100
2106
  }
2101
2107
  for (const [index, entry] of config.plugins.flatMap((plugin) => plugin.clientEntries ?? []).entries()) {
2102
2108
  const localName = `pluginClientInitializer${index}`;
@@ -2105,41 +2111,123 @@ function createClientModule(config, development) {
2105
2111
  `import ${importTarget} from ${JSON.stringify(entry.moduleSpecifier)};`
2106
2112
  );
2107
2113
  const publicConfig = entry.publicConfig;
2108
- const initializer = publicConfig === void 0 ? `${localName}()` : `${localName}(${serializePublicClientConfig(publicConfig)})`;
2114
+ const serializedConfig = publicConfig === void 0 ? "undefined" : serializePublicClientConfig(publicConfig);
2115
+ const initializer = entry.initializerArguments === "context" ? `${localName}(context)` : entry.initializerArguments === "root" ? `${localName}(context.root)` : `${localName}(context.config)`;
2116
+ const initializers = entry.scope === "app" ? appInitializers : pageInitializers;
2109
2117
  initializers.push(
2110
- `runPluginClientInitializer(${JSON.stringify(entry.moduleSpecifier)}, () => ${initializer})`
2118
+ `mountPluginClientInitializer(${JSON.stringify(entry.scope ?? "page")}, ${JSON.stringify(entry.moduleSpecifier)}, root, ${serializedConfig}, (context) => ${initializer})`
2111
2119
  );
2112
2120
  }
2113
2121
  return `${imports.join("\n")}
2114
2122
 
2115
- function runPluginClientInitializer(name, initialize) {
2116
- try {
2117
- const result = initialize();
2118
- if (result && typeof result.then === "function") {
2119
- result.catch((error) => reportPluginClientError(name, error));
2120
- }
2121
- } catch (error) {
2122
- reportPluginClientError(name, error);
2123
- }
2123
+ const lifecycle = new ClientLifecycle();
2124
+ let stopSpaNavigation;
2125
+
2126
+ function mountPluginClientInitializer(scope, name, root, config, initialize) {
2127
+ void lifecycle.mount({ scope, name, root, config, initialize });
2128
+ }
2129
+
2130
+ function mountAppClientInitializers(root) {
2131
+ ${appInitializers.map((initializer) => ` ${initializer};`).join("\n")}
2124
2132
  }
2125
2133
 
2126
- function reportPluginClientError(name, error) {
2127
- console.error("[riebeckite] Plugin client initializer failed: " + name, error);
2134
+ function mountPageClientInitializers(root) {
2135
+ ${pageInitializers.map((initializer) => ` ${initializer};`).join("\n")}
2128
2136
  }
2129
2137
 
2130
2138
  export function initRiebeckiteClient() {
2131
- ${initializers.map((initializer) => ` ${initializer};`).join("\n")}
2139
+ mountAppClientInitializers(document);
2140
+ mountPageClientInitializers(document.querySelector("#main-content") ?? document);
2141
+ ${config.spa === void 0 ? "" : ` stopSpaNavigation ??= startSpaNavigation({
2142
+ ...${serializePublicClientConfig(config.spa)},
2143
+ onBeforePageCommit: () => lifecycle.unmount("page"),
2144
+ onPageCommit: async ({ root, signal }) => {
2145
+ if (signal.aborted) return;
2146
+ await createClient();
2147
+ if (signal.aborted || !root.isConnected) return;
2148
+ mountPageClientInitializers(root);
2149
+ },
2150
+ });`}
2151
+ }
2152
+
2153
+ export async function remountRiebeckitePage(root) {
2154
+ await lifecycle.unmount("page");
2155
+ mountPageClientInitializers(root);
2156
+ }
2157
+
2158
+ export async function disposeRiebeckiteClient() {
2159
+ stopSpaNavigation?.();
2160
+ stopSpaNavigation = undefined;
2161
+ await lifecycle.dispose();
2132
2162
  }
2133
2163
  `;
2134
2164
  }
2135
2165
 
2136
2166
  // src/development_inspector.ts
2137
- import { collectDevelopmentDiagnostics } from "@riebeckite/diagnostics";
2167
+ import {
2168
+ buildOutputPlan as buildOutputPlan2,
2169
+ ContentManager,
2170
+ FileSystemContentSource
2171
+ } from "@riebeckite/core";
2172
+ import {
2173
+ collectDevelopmentDiagnostics,
2174
+ diagnoseOutputPlan,
2175
+ sortDevelopmentDiagnostics
2176
+ } from "@riebeckite/diagnostics";
2138
2177
  var clientModuleId2 = "virtual:riebeckite/development-inspector";
2139
2178
  var resolvedClientModuleId2 = `\0${clientModuleId2}`;
2140
- var endpointPath = "/__riebeckite/inspector/report";
2179
+ var reportPath = "/__riebeckite/inspector/report";
2180
+ var outputPath = "/__riebeckite/inspector/output";
2181
+ var listedOutputLimit = 20;
2141
2182
  function riebeckiteDevelopmentInspector(getApplication) {
2142
2183
  let revision = 0;
2184
+ let inspection = null;
2185
+ const inspectApplication = (application, currentRevision) => {
2186
+ if (inspection?.revision === currentRevision && inspection.application === application) {
2187
+ return inspection;
2188
+ }
2189
+ if (inspection) {
2190
+ const previous = inspection;
2191
+ const disposePrevious = () => {
2192
+ void previous.dispose().catch(() => {
2193
+ });
2194
+ };
2195
+ void Promise.allSettled([previous.report, previous.output]).then(
2196
+ disposePrevious
2197
+ );
2198
+ inspection = null;
2199
+ }
2200
+ const content = createInspectionContent(application);
2201
+ let disposed = false;
2202
+ const dispose = async () => {
2203
+ if (disposed) return;
2204
+ disposed = true;
2205
+ await content.dispose();
2206
+ };
2207
+ const report = collectDevelopmentDiagnostics({
2208
+ config: application.config,
2209
+ content,
2210
+ revision: currentRevision,
2211
+ includeOutput: false
2212
+ });
2213
+ const output = report.then(
2214
+ () => inspectContentOutputs(content),
2215
+ () => inspectContentOutputs(content)
2216
+ );
2217
+ const entry = {
2218
+ application,
2219
+ revision: currentRevision,
2220
+ report,
2221
+ output,
2222
+ dispose
2223
+ };
2224
+ inspection = entry;
2225
+ void report.catch(() => {
2226
+ if (inspection === entry) inspection = null;
2227
+ void output.then(dispose, dispose);
2228
+ });
2229
+ return entry;
2230
+ };
2143
2231
  return {
2144
2232
  name: "riebeckite-development-inspector",
2145
2233
  apply: "serve",
@@ -2151,7 +2239,7 @@ function riebeckiteDevelopmentInspector(getApplication) {
2151
2239
  return id === resolvedClientModuleId2 ? inspectorClientModule() : null;
2152
2240
  },
2153
2241
  configureServer(server) {
2154
- const middleware = async (request, response) => {
2242
+ const reportMiddleware = async (request, response) => {
2155
2243
  if (!isLocalRequest(request.headers.host, server)) {
2156
2244
  response.statusCode = 403;
2157
2245
  response.end();
@@ -2159,25 +2247,16 @@ function riebeckiteDevelopmentInspector(getApplication) {
2159
2247
  }
2160
2248
  try {
2161
2249
  const application = getApplication();
2162
- const report = await collectDevelopmentDiagnostics({
2163
- config: application.config,
2164
- revision
2165
- });
2250
+ const report = await inspectApplication(application, revision).report;
2166
2251
  response.setHeader("content-type", "application/json; charset=utf-8");
2167
2252
  response.setHeader("cache-control", "no-store");
2168
- const currentPage = currentPageFromRequest(request.url);
2169
2253
  response.end(
2170
2254
  JSON.stringify({
2171
2255
  ...report,
2172
2256
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2173
- currentPage,
2257
+ currentPage: currentPageFromRequest(request.url),
2174
2258
  theme: application.config.theme.name,
2175
- plugins: application.config.plugins.map((plugin) => plugin.name),
2176
- capabilities: {
2177
- dependencies: "unavailable",
2178
- output: "unavailable",
2179
- pageMetadata: "unavailable"
2180
- }
2259
+ plugins: application.config.plugins.map((plugin) => plugin.name)
2181
2260
  })
2182
2261
  );
2183
2262
  } catch {
@@ -2186,8 +2265,39 @@ function riebeckiteDevelopmentInspector(getApplication) {
2186
2265
  response.end(JSON.stringify({ code: "INSPECTOR_REPORT_FAILED" }));
2187
2266
  }
2188
2267
  };
2189
- server.middlewares.use(endpointPath, middleware);
2190
- return () => prioritizeMiddleware(server, middleware);
2268
+ const outputMiddleware = async (request, response) => {
2269
+ if (!isLocalRequest(request.headers.host, server)) {
2270
+ response.statusCode = 403;
2271
+ response.end();
2272
+ return;
2273
+ }
2274
+ try {
2275
+ const application = getApplication();
2276
+ const entry = inspectApplication(application, revision);
2277
+ const result = await entry.output;
2278
+ response.setHeader("content-type", "application/json; charset=utf-8");
2279
+ response.setHeader("cache-control", "no-store");
2280
+ response.end(
2281
+ JSON.stringify({
2282
+ version: 1,
2283
+ revision: entry.revision,
2284
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2285
+ ...result,
2286
+ diagnostics: sortDevelopmentDiagnostics(result.diagnostics)
2287
+ })
2288
+ );
2289
+ } catch {
2290
+ response.statusCode = 500;
2291
+ response.setHeader("content-type", "application/json; charset=utf-8");
2292
+ response.end(JSON.stringify({ code: "INSPECTOR_OUTPUT_FAILED" }));
2293
+ }
2294
+ };
2295
+ server.middlewares.use(reportPath, reportMiddleware);
2296
+ server.middlewares.use(outputPath, outputMiddleware);
2297
+ return () => {
2298
+ prioritizeMiddleware(server, outputMiddleware, outputPath);
2299
+ prioritizeMiddleware(server, reportMiddleware, reportPath);
2300
+ };
2191
2301
  },
2192
2302
  handleHotUpdate({ server }) {
2193
2303
  revision += 1;
@@ -2195,16 +2305,142 @@ function riebeckiteDevelopmentInspector(getApplication) {
2195
2305
  }
2196
2306
  };
2197
2307
  }
2308
+ function createInspectionContent(application) {
2309
+ const { config, contentRoot } = application;
2310
+ return new ContentManager(
2311
+ config.content.source ?? new FileSystemContentSource(contentRoot, config.content.exclude),
2312
+ config.content.exclude,
2313
+ { config, commitBuildState: false }
2314
+ );
2315
+ }
2316
+ async function inspectContentOutputs(content) {
2317
+ try {
2318
+ const changes = await content.getOutputChangeSet();
2319
+ const plan = buildOutputPlan2({
2320
+ inventory: [...changes.affected, ...changes.unchanged],
2321
+ removed: changes.removed,
2322
+ affectedPaths: new Set(changes.affected.map((output) => output.path)),
2323
+ cacheAvailable: changes.unchanged.length > 0
2324
+ });
2325
+ return {
2326
+ output: inspectOutput(changes, plan),
2327
+ dependencies: inspectDependencies(changes),
2328
+ diagnostics: diagnoseOutputPlan(plan)
2329
+ };
2330
+ } catch {
2331
+ return {
2332
+ output: {
2333
+ status: "unavailable",
2334
+ reason: "output inspection failed"
2335
+ },
2336
+ dependencies: {
2337
+ status: "unavailable",
2338
+ reason: "dependency inspection failed"
2339
+ },
2340
+ diagnostics: []
2341
+ };
2342
+ }
2343
+ }
2344
+ function inspectOutput(changes, plan) {
2345
+ return {
2346
+ status: "available",
2347
+ candidateOutputCount: changes.candidateOutputCount,
2348
+ affectedOutputCount: changes.affectedOutputCount,
2349
+ unchangedOutputCount: changes.unchangedOutputCount,
2350
+ removedOutputCount: changes.removedOutputCount,
2351
+ fullRegenerationRequired: changes.fullRegenerationRequired,
2352
+ conflicts: plan.conflicts.map((conflict) => ({
2353
+ path: conflict.path,
2354
+ owners: conflict.owners
2355
+ })),
2356
+ affected: changes.affected.slice(0, listedOutputLimit).map(listOutput),
2357
+ hiddenAffectedCount: Math.max(
2358
+ 0,
2359
+ changes.affected.length - listedOutputLimit
2360
+ ),
2361
+ removed: changes.removed.slice(0, listedOutputLimit).map(listOutput),
2362
+ hiddenRemovedCount: Math.max(0, changes.removed.length - listedOutputLimit)
2363
+ };
2364
+ }
2365
+ function inspectDependencies(changes) {
2366
+ const outputs = [...changes.affected, ...changes.unchanged];
2367
+ const kinds = /* @__PURE__ */ new Map();
2368
+ const listed = [];
2369
+ let dependencyCount = 0;
2370
+ let dependentOutputCount = 0;
2371
+ let unknownOutputCount = 0;
2372
+ let hiddenOutputCount = 0;
2373
+ for (const output of outputs) {
2374
+ for (const dependency of output.dependencies) {
2375
+ dependencyCount += 1;
2376
+ kinds.set(dependency.type, (kinds.get(dependency.type) ?? 0) + 1);
2377
+ }
2378
+ if (output.dependencies.some(({ type }) => type === "unknown")) {
2379
+ unknownOutputCount += 1;
2380
+ }
2381
+ if (output.dependencies.length === 0) continue;
2382
+ dependentOutputCount += 1;
2383
+ if (listed.length < listedOutputLimit) {
2384
+ listed.push({
2385
+ path: output.path,
2386
+ dependencies: output.dependencies.map(dependencyLabel)
2387
+ });
2388
+ } else {
2389
+ hiddenOutputCount += 1;
2390
+ }
2391
+ }
2392
+ return {
2393
+ status: "available",
2394
+ outputCount: outputs.length,
2395
+ dependentOutputCount,
2396
+ dependencyCount,
2397
+ unknownOutputCount,
2398
+ kinds: [...kinds.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([kind, count]) => ({ kind, count })),
2399
+ outputs: listed,
2400
+ hiddenOutputCount
2401
+ };
2402
+ }
2403
+ function listOutput(output) {
2404
+ return {
2405
+ path: output.path,
2406
+ kind: output.kind,
2407
+ producer: output.producer
2408
+ };
2409
+ }
2410
+ function dependencyLabel(dependency) {
2411
+ switch (dependency.type) {
2412
+ case "content":
2413
+ return `content:${dependency.slug}`;
2414
+ case "tag":
2415
+ return `tag:${dependency.tag}`;
2416
+ case "folder":
2417
+ return `folder:${dependency.folder}`;
2418
+ case "file":
2419
+ return `file:${dependency.path}`;
2420
+ case "public-location":
2421
+ return `public-location:${dependency.pathname}`;
2422
+ case "projection":
2423
+ return `projection:${dependency.projection}`;
2424
+ case "external":
2425
+ return `external:${dependency.id}`;
2426
+ case "content-query":
2427
+ return `content-query:${dependency.properties.join(",")}${dependency.unknown ? "?" : ""}`;
2428
+ case "global":
2429
+ return "global";
2430
+ case "unknown":
2431
+ return "unknown";
2432
+ }
2433
+ }
2198
2434
  function currentPageFromRequest(url) {
2199
- const value = new URL(url ?? endpointPath, "http://localhost").searchParams.get(
2435
+ const value = new URL(url ?? reportPath, "http://localhost").searchParams.get(
2200
2436
  "path"
2201
2437
  );
2202
- return value && value.startsWith("/") && !value.startsWith("//") ? value : "/";
2438
+ return value?.startsWith("/") && !value.startsWith("//") ? value : "/";
2203
2439
  }
2204
- function prioritizeMiddleware(server, middleware) {
2440
+ function prioritizeMiddleware(server, middleware, path11) {
2205
2441
  const stack = server.middlewares.stack;
2206
2442
  const index = stack?.findIndex(
2207
- (layer) => layer.handle === middleware || layer.route === endpointPath
2443
+ (layer) => layer.handle === middleware || layer.route === path11
2208
2444
  ) ?? -1;
2209
2445
  if (index > 0 && stack) stack.unshift(...stack.splice(index, 1));
2210
2446
  }
@@ -2218,7 +2454,8 @@ function isLocalRequest(host, server) {
2218
2454
  }
2219
2455
  function inspectorClientModule() {
2220
2456
  return `
2221
- const endpoint = ${JSON.stringify(endpointPath)};
2457
+ const reportEndpoint = ${JSON.stringify(reportPath)};
2458
+ const outputEndpoint = ${JSON.stringify(outputPath)};
2222
2459
  const panelId = "riebeckite-development-inspector";
2223
2460
 
2224
2461
  export function initDevelopmentInspector() {
@@ -2228,23 +2465,57 @@ export function initDevelopmentInspector() {
2228
2465
  let generation = 0;
2229
2466
  let report;
2230
2467
  let activeTab = 0;
2468
+ let refreshing = false;
2469
+ let pendingRefresh = false;
2470
+ let loadingTimer;
2231
2471
  const tabs = ["Overview", "Diagnostics", "Dependencies", "Plugins", "Output"];
2232
2472
  const refresh = async () => {
2233
2473
  if (!panel?.isConnected) return;
2474
+ if (refreshing) {
2475
+ pendingRefresh = true;
2476
+ return;
2477
+ }
2478
+ refreshing = true;
2234
2479
  controller?.abort();
2235
2480
  controller = new AbortController();
2236
2481
  const current = ++generation;
2237
2482
  try {
2238
- const response = await fetch(endpoint + "?path=" + encodeURIComponent(location.pathname), { cache: "no-store", signal: controller.signal });
2483
+ const response = await fetch(reportEndpoint + "?path=" + encodeURIComponent(location.pathname), { cache: "no-store", signal: controller.signal });
2239
2484
  if (!response.ok) throw new Error("Inspector report is unavailable.");
2240
2485
  const next = await response.json();
2241
2486
  if (current !== generation || !panel?.isConnected) return;
2242
2487
  report = next;
2243
2488
  render();
2489
+ void loadOutput(current);
2490
+ } catch (error) {
2491
+ if (error.name === "AbortError" || current !== generation) return;
2492
+ clearInterval(loadingTimer);
2493
+ renderFailure(panel.shadowRoot, error instanceof Error ? error.message : String(error));
2494
+ panel.shadowRoot.querySelector("[data-close]").addEventListener("click", close);
2495
+ } finally {
2496
+ refreshing = false;
2497
+ if (pendingRefresh) {
2498
+ pendingRefresh = false;
2499
+ void refresh();
2500
+ }
2501
+ }
2502
+ };
2503
+ const loadOutput = async (current) => {
2504
+ try {
2505
+ const response = await fetch(outputEndpoint, { cache: "no-store", signal: controller.signal });
2506
+ if (!response.ok) throw new Error("Inspector output is unavailable.");
2507
+ const next = await response.json();
2508
+ if (current !== generation || !panel?.isConnected) return;
2509
+ report.output = next.output;
2510
+ report.dependencies = next.dependencies;
2511
+ report.diagnostics = report.diagnostics.concat(next.diagnostics);
2512
+ report.summary = summarize(report.diagnostics);
2513
+ render();
2244
2514
  } catch (error) {
2245
2515
  if (error.name === "AbortError" || current !== generation) return;
2246
- renderFailure(panel.shadowRoot, error instanceof Error ? error.message : String(error));
2247
- panel.shadowRoot.querySelector("[data-close]").addEventListener("click", close);
2516
+ report.output = { status: "unavailable", reason: "output inspection failed" };
2517
+ report.dependencies = { status: "unavailable", reason: "dependency inspection failed" };
2518
+ render();
2248
2519
  }
2249
2520
  };
2250
2521
  const open = () => {
@@ -2252,16 +2523,30 @@ export function initDevelopmentInspector() {
2252
2523
  panel = document.createElement("div");
2253
2524
  panel.id = panelId;
2254
2525
  panel.attachShadow({ mode: "open" });
2255
- document.body.append(panel);
2256
2526
  }
2257
- refresh();
2527
+ if (!panel.isConnected) document.body.append(panel);
2528
+ showLoading();
2529
+ void refresh();
2258
2530
  };
2259
2531
  const close = () => {
2260
2532
  generation += 1;
2533
+ pendingRefresh = false;
2261
2534
  controller?.abort();
2535
+ clearInterval(loadingTimer);
2262
2536
  panel?.remove();
2263
2537
  launcher?.focus();
2264
2538
  };
2539
+ const showLoading = () => {
2540
+ if (!panel) return;
2541
+ clearInterval(loadingTimer);
2542
+ panel.shadowRoot.innerHTML = '<style>' + styles + '</style><section role="dialog" aria-modal="false" aria-labelledby="rb-loading-title"><header><strong id="rb-loading-title">Riebeckite Inspector</strong><button type="button" data-close aria-label="Close inspector">Close</button></header><p class="status" aria-live="polite">Loading inspector report…</p></section>';
2543
+ const status = panel.shadowRoot.querySelector(".status");
2544
+ const started = Date.now();
2545
+ loadingTimer = setInterval(() => {
2546
+ if (status?.isConnected) status.textContent = 'Loading inspector report… ' + Math.round((Date.now() - started) / 1000) + 's';
2547
+ }, 1000);
2548
+ panel.shadowRoot.querySelector("[data-close]").addEventListener("click", close);
2549
+ };
2265
2550
  launcher = document.createElement("button");
2266
2551
  launcher.type = "button";
2267
2552
  launcher.textContent = "Inspector";
@@ -2282,8 +2567,9 @@ export function initDevelopmentInspector() {
2282
2567
  import.meta.hot?.on("riebeckite:inspector", refresh);
2283
2568
 
2284
2569
  function render() {
2570
+ clearInterval(loadingTimer);
2285
2571
  const root = panel.shadowRoot;
2286
- root.innerHTML = '<style>' + styles + '</style><section role="dialog" aria-modal="false" aria-labelledby="rb-title" tabindex="-1"><header><div><strong id="rb-title">Riebeckite Inspector</strong><p>Ctrl+Alt+R</p></div><button type="button" data-close aria-label="Close inspector">Close</button></header><div class="summary"><b>' + report.summary.errors + '</b> errors <b>' + report.summary.warnings + '</b> warnings <b>' + report.summary.info + '</b> info</div><div role="tablist" aria-label="Inspector sections">' + tabs.map((tab, index) => '<button id="rb-tab-' + index + '" role="tab" aria-controls="rb-panel-' + index + '" aria-selected="' + (index === activeTab) + '" tabindex="' + (index === activeTab ? 0 : -1) + '" data-tab="' + index + '">' + tab + '</button>').join("") + '</div><div id="rb-panel-' + activeTab + '" role="tabpanel" aria-labelledby="rb-tab-' + activeTab + '" tabindex="0">' + panelContent() + '</div><p class="status" aria-live="polite">Inspector report updated</p></section>';
2572
+ root.innerHTML = '<style>' + styles + '</style><section role="dialog" aria-modal="false" aria-labelledby="rb-title" tabindex="-1"><header><div><strong id="rb-title">Riebeckite Inspector</strong><p>Ctrl+Alt+R</p></div><button type="button" data-close aria-label="Close inspector">Close</button></header><div class="summary"><b>' + report.summary.errors + '</b> errors <b>' + report.summary.warnings + '</b> warnings <b>' + report.summary.info + '</b> info</div><div role="tablist" aria-label="Inspector sections">' + tabs.map((tab, index) => '<button id="rb-tab-' + index + '" role="tab" aria-controls="rb-panel-' + index + '" aria-selected="' + (index === activeTab) + '" tabindex="' + (index === activeTab ? 0 : -1) + '" data-tab="' + index + '">' + tab + '</button>').join("") + '</div><div id="rb-panel-' + activeTab + '" role="tabpanel" aria-labelledby="rb-tab-' + activeTab + '" tabindex="0">' + panelContent() + '</div><p class="status" aria-live="polite">' + (report.output ? "Inspector report updated" : "Inspector report updated · loading output plan…") + '</p></section>';
2287
2573
  root.querySelector("[data-close]").addEventListener("click", close);
2288
2574
  root.querySelectorAll("[data-tab]").forEach((button) => button.addEventListener("click", () => { activeTab = Number(button.dataset.tab); render(); }));
2289
2575
  root.querySelector("[role=tablist]").addEventListener("keydown", (event) => {
@@ -2298,20 +2584,78 @@ export function initDevelopmentInspector() {
2298
2584
  function panelContent() {
2299
2585
  if (activeTab === 0) return '<dl><dt>Page</dt><dd>' + escapeHtml(report.currentPage) + '</dd><dt>Theme</dt><dd>' + escapeHtml(report.theme) + '</dd><dt>Revision</dt><dd>' + escapeHtml(report.revision) + '</dd></dl>';
2300
2586
  if (activeTab === 1) return diagnosticList(report.diagnostics);
2301
- if (activeTab === 2) return unavailable("Dependencies", report.capabilities.dependencies);
2587
+ if (activeTab === 2) return dependencyReport(report.dependencies);
2302
2588
  if (activeTab === 3) return '<ul>' + (report.plugins.map((plugin) => '<li>' + escapeHtml(plugin) + '</li>').join("") || "<li>No plugins configured.</li>") + '</ul>';
2303
- return unavailable("Output", report.capabilities.output);
2589
+ return outputReport(report.output);
2304
2590
  }
2305
2591
  }
2306
2592
 
2593
+ function summarize(diagnostics) {
2594
+ return {
2595
+ errors: diagnostics.filter((diagnostic) => diagnostic.severity === "error").length,
2596
+ warnings: diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length,
2597
+ info: diagnostics.filter((diagnostic) => diagnostic.severity === "info").length,
2598
+ };
2599
+ }
2600
+
2307
2601
  function diagnosticList(diagnostics) {
2308
2602
  return '<ul>' + (diagnostics.map((diagnostic) =>
2309
2603
  '<li data-severity="' + escapeHtml(diagnostic.severity) + '"><b>' + escapeHtml(diagnostic.code) + '</b> ' + escapeHtml(diagnostic.message) + location(diagnostic) + '</li>'
2310
2604
  ).join("") || "<li>No diagnostic findings.</li>") + '</ul>';
2311
2605
  }
2312
2606
 
2607
+ function outputReport(output) {
2608
+ if (!output) return '<p class="status" aria-live="polite">Loading output plan…</p>';
2609
+ if (output.status !== "available") return unavailable("Output", output.reason);
2610
+ return '<dl>' +
2611
+ '<dt>Candidate</dt><dd>' + output.candidateOutputCount + '</dd>' +
2612
+ '<dt>Affected</dt><dd>' + output.affectedOutputCount + '</dd>' +
2613
+ '<dt>Unchanged</dt><dd>' + output.unchangedOutputCount + '</dd>' +
2614
+ '<dt>Removed</dt><dd>' + output.removedOutputCount + '</dd>' +
2615
+ '<dt>Full regeneration</dt><dd>' + (output.fullRegenerationRequired ? "required" : "not required") + '</dd>' +
2616
+ '<dt>Owner conflicts</dt><dd>' + output.conflicts.length + '</dd>' +
2617
+ '</dl>' +
2618
+ listedOutputs("Affected outputs", output.affected, output.hiddenAffectedCount) +
2619
+ listedOutputs("Removed outputs", output.removed, output.hiddenRemovedCount) +
2620
+ conflictList(output.conflicts);
2621
+ }
2622
+
2623
+ function listedOutputs(title, outputs, hiddenCount) {
2624
+ if (outputs.length === 0) return "";
2625
+ return '<h3>' + escapeHtml(title) + '</h3><ul>' + outputs.map((output) =>
2626
+ '<li><code>' + escapeHtml(output.path) + '</code> <small>' + escapeHtml(output.kind) + ' · ' + escapeHtml(output.producer) + '</small></li>'
2627
+ ).join("") + '</ul>' + (hiddenCount > 0 ? '<p class="more">' + hiddenCount + ' more not shown.</p>' : "");
2628
+ }
2629
+
2630
+ function conflictList(conflicts) {
2631
+ if (conflicts.length === 0) return "";
2632
+ return '<h3>Owner conflicts</h3><ul>' + conflicts.map((conflict) =>
2633
+ '<li><code>' + escapeHtml(conflict.path) + '</code> <small>' + escapeHtml(conflict.owners.join(", ")) + '</small></li>'
2634
+ ).join("") + '</ul>';
2635
+ }
2636
+
2637
+ function dependencyReport(dependencies) {
2638
+ if (!dependencies) return '<p class="status" aria-live="polite">Loading dependencies…</p>';
2639
+ if (dependencies.status !== "available") return unavailable("Dependencies", dependencies.reason);
2640
+ return '<dl>' +
2641
+ '<dt>Outputs</dt><dd>' + dependencies.outputCount + '</dd>' +
2642
+ '<dt>With dependencies</dt><dd>' + dependencies.dependentOutputCount + '</dd>' +
2643
+ '<dt>Dependencies</dt><dd>' + dependencies.dependencyCount + '</dd>' +
2644
+ '<dt>Unknown outputs</dt><dd>' + dependencies.unknownOutputCount + '</dd>' +
2645
+ '</dl>' +
2646
+ (dependencies.kinds.length === 0 ? "<p>No output dependencies.</p>" :
2647
+ '<h3>Kinds</h3><ul>' + dependencies.kinds.map((kind) =>
2648
+ '<li>' + escapeHtml(kind.kind) + ' <small>' + kind.count + '</small></li>'
2649
+ ).join("") + '</ul>') +
2650
+ (dependencies.outputs.length === 0 ? "" :
2651
+ '<h3>Outputs</h3><ul>' + dependencies.outputs.map((output) =>
2652
+ '<li><code>' + escapeHtml(output.path) + '</code><br><small>' + escapeHtml(output.dependencies.join(", ")) + '</small></li>'
2653
+ ).join("") + '</ul>') +
2654
+ (dependencies.hiddenOutputCount > 0 ? '<p class="more">' + dependencies.hiddenOutputCount + ' more outputs not shown.</p>' : "");
2655
+ }
2656
+
2313
2657
  function unavailable(subject, capability) {
2314
- return '<p class="partial">' + escapeHtml(subject) + ': ' + escapeHtml(capability) + ' in the development inspector.</p>';
2658
+ return '<p class="partial">' + escapeHtml(subject) + ' unavailable: ' + escapeHtml(capability) + '.</p>';
2315
2659
  }
2316
2660
 
2317
2661
  function renderFailure(root, message) {
@@ -2327,7 +2671,7 @@ function escapeHtml(value) {
2327
2671
  return String(value).replace(/[&<>"]/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[character]);
2328
2672
  }
2329
2673
 
2330
- const styles = ':host{all:initial}section{position:fixed;right:16px;bottom:56px;z-index:2147483647;width:min(540px,calc(100vw - 32px));max-height:70vh;overflow:auto;background:#171717;color:#f4f4f4;border:1px solid #555;padding:16px;font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;box-shadow:0 8px 24px #0006}header{display:flex;justify-content:space-between;gap:16px;border-bottom:1px solid #555;padding-bottom:8px}header p,.status,small{color:#b8b8b8;margin:2px 0}.summary{margin:12px 0}button{border:1px solid #666;background:#242424;color:#fff;padding:6px 8px;cursor:pointer}button[aria-selected="true"]{border-color:#e2c57b;color:#ffe08a}.partial{color:#e2c57b}ul{margin:12px 0;padding-left:20px}li{margin:8px 0}li[data-severity="error"]{color:#ff9797}li[data-severity="warning"]{color:#ffe08a}dl{display:grid;grid-template-columns:max-content 1fr;gap:8px 16px}dt{color:#b8b8b8}.failure{color:#ff9797}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}@media(max-width:560px){section{right:8px;bottom:52px;width:calc(100vw - 32px);max-height:75vh}}';
2674
+ const styles = ':host{all:initial}section{position:fixed;right:16px;bottom:56px;z-index:2147483647;width:min(540px,calc(100vw - 32px));max-height:70vh;overflow:auto;background:#171717;color:#f4f4f4;border:1px solid #555;padding:16px;font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;box-shadow:0 8px 24px #0006}header{display:flex;justify-content:space-between;gap:16px;border-bottom:1px solid #555;padding-bottom:8px}header p,.status,small{color:#b8b8b8;margin:2px 0}.summary{margin:12px 0}button{border:1px solid #666;background:#242424;color:#fff;padding:6px 8px;cursor:pointer}button[aria-selected="true"]{border-color:#e2c57b;color:#ffe08a}.partial{color:#e2c57b}.more{color:#b8b8b8;margin:4px 0}h3{color:#b8b8b8;font-size:12px;margin:12px 0 4px;text-transform:uppercase}ul{margin:12px 0;padding-left:20px}li{margin:8px 0}li[data-severity="error"]{color:#ff9797}li[data-severity="warning"]{color:#ffe08a}dl{display:grid;grid-template-columns:max-content 1fr;gap:8px 16px}dt{color:#b8b8b8}.failure{color:#ff9797}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}@media(max-width:560px){section{right:8px;bottom:52px;width:calc(100vw - 32px);max-height:75vh}}';
2331
2675
  `;
2332
2676
  }
2333
2677
 
@@ -2340,6 +2684,14 @@ var viteConfigFileNames = [
2340
2684
  "vite.config.js",
2341
2685
  "vite.config.mjs"
2342
2686
  ];
2687
+ var ignoredApplicationRootDirectoryNames = /* @__PURE__ */ new Set([
2688
+ ".git",
2689
+ ".riebeckite",
2690
+ "node_modules",
2691
+ "test",
2692
+ "tests",
2693
+ "templates"
2694
+ ]);
2343
2695
  async function resolveHonoxApplicationRoot(configRoot, startDirectory) {
2344
2696
  const roots = await findViteApplicationRoots(configRoot, startDirectory);
2345
2697
  if (roots.length === 1) return roots[0];
@@ -2402,12 +2754,24 @@ async function startHonoxDevServer(options) {
2402
2754
  });
2403
2755
  }
2404
2756
  await server.listen(options.server?.port);
2405
- options.onServerReady?.([
2757
+ const urls = [
2406
2758
  ...server.resolvedUrls?.local ?? [],
2407
2759
  ...server.resolvedUrls?.network ?? []
2408
- ]);
2409
- server.printUrls();
2410
- await waitForShutdown(server);
2760
+ ];
2761
+ const shutdown = waitForShutdown(server);
2762
+ try {
2763
+ server.printUrls();
2764
+ await options.onServerReady?.(urls);
2765
+ } catch (error) {
2766
+ shutdown.dispose();
2767
+ if (shutdown.isClosing()) {
2768
+ await shutdown.promise;
2769
+ return;
2770
+ }
2771
+ await server.close();
2772
+ throw error;
2773
+ }
2774
+ await shutdown.promise;
2411
2775
  });
2412
2776
  }
2413
2777
  async function buildHonoxApplication(options) {
@@ -2438,14 +2802,28 @@ async function withWorkingDirectory(root, task) {
2438
2802
  async function runTraced(tracer, name, buildTask) {
2439
2803
  return tracer ? await tracer.span(name, {}, buildTask) : await buildTask();
2440
2804
  }
2441
- async function waitForShutdown(server) {
2442
- await new Promise((resolve3) => {
2443
- const close = () => {
2444
- void server.close().finally(resolve3);
2445
- };
2446
- process.once("SIGINT", close);
2447
- process.once("SIGTERM", close);
2805
+ function waitForShutdown(server) {
2806
+ let resolve3 = () => {
2807
+ };
2808
+ let closing = false;
2809
+ const close = () => {
2810
+ if (closing) return;
2811
+ closing = true;
2812
+ void server.close().finally(resolve3);
2813
+ };
2814
+ const promise = new Promise((resolvePromise) => {
2815
+ resolve3 = resolvePromise;
2448
2816
  });
2817
+ process.once("SIGINT", close);
2818
+ process.once("SIGTERM", close);
2819
+ return {
2820
+ promise,
2821
+ isClosing: () => closing,
2822
+ dispose: () => {
2823
+ process.removeListener("SIGINT", close);
2824
+ process.removeListener("SIGTERM", close);
2825
+ }
2826
+ };
2449
2827
  }
2450
2828
  async function findViteApplicationRoots(root, startDirectory) {
2451
2829
  const applicationRoot = startDirectory ? await findViteApplicationRootFrom(startDirectory, root) : void 0;
@@ -2458,7 +2836,7 @@ async function findViteApplicationRoots(root, startDirectory) {
2458
2836
  if (await hasViteConfig(directory)) applicationRoots.push(directory);
2459
2837
  const entries = await fs7.readdir(directory, { withFileTypes: true });
2460
2838
  for (const entry of entries) {
2461
- if (!entry.isDirectory() || entry.name === "node_modules" || entry.name === ".git" || entry.name === "tests") {
2839
+ if (!entry.isDirectory() || ignoredApplicationRootDirectoryNames.has(entry.name)) {
2462
2840
  continue;
2463
2841
  }
2464
2842
  directories.push(path9.join(directory, entry.name));
@@ -1,3 +1,3 @@
1
1
  import type { ResolvedRiebeckiteConfig } from "@riebeckite/core";
2
2
  import type { Plugin } from "vite";
3
- export declare function riebeckiteClientModule(getConfig: () => Pick<ResolvedRiebeckiteConfig, "plugins">): Plugin;
3
+ export declare function riebeckiteClientModule(getConfig: () => Pick<ResolvedRiebeckiteConfig, "plugins" | "spa">): Plugin;
@@ -128,20 +128,26 @@ function PluginHeadTags(props) {
128
128
  function renderPluginHeadTag(tag, index) {
129
129
  const key = `${tag.tag}-${index}`;
130
130
  if (tag.tag === "meta") {
131
- return /* @__PURE__ */ createElement("meta", { ...tag.attrs, key });
131
+ return /* @__PURE__ */ createElement("meta", { ...tag.attrs, "data-riebeckite-head": "page", key });
132
132
  }
133
133
  if (tag.tag === "link") {
134
- return /* @__PURE__ */ createElement("link", { ...tag.attrs, key });
134
+ return /* @__PURE__ */ createElement("link", { ...tag.attrs, "data-riebeckite-head": "page", key });
135
135
  }
136
136
  return /* @__PURE__ */ createElement(
137
137
  "script",
138
138
  {
139
139
  ...tag.attrs,
140
+ "data-riebeckite-head": isDataScript(tag) ? "page" : void 0,
140
141
  key,
141
142
  dangerouslySetInnerHTML: tag.children ? { __html: tag.children } : void 0
142
143
  }
143
144
  );
144
145
  }
146
+ function isDataScript(tag) {
147
+ if (tag.tag !== "script") return false;
148
+ const type = tag.attrs?.type?.trim().toLowerCase();
149
+ return type === "application/ld+json" || type === "application/json";
150
+ }
145
151
  function RiebeckiteHead(props) {
146
152
  const site = useSiteContext();
147
153
  const faviconHref = props.faviconHref === void 0 ? "/favicon.ico" : props.faviconHref;
@@ -8,7 +8,7 @@ export type HonoxApplicationOptions = {
8
8
  open?: boolean;
9
9
  port?: number;
10
10
  };
11
- onServerReady?: (urls: readonly string[]) => void;
11
+ onServerReady?: (urls: readonly string[]) => void | Promise<void>;
12
12
  restartOnChange?: readonly string[];
13
13
  };
14
14
  export type ResolvedHonoxApplication = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/honox",
3
- "version": "0.0.31",
3
+ "version": "0.0.32",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -44,14 +44,15 @@
44
44
  "hono": "^4.12.25",
45
45
  "honox": "0.1.56",
46
46
  "vite": "^8.0.9",
47
- "@riebeckite/core": "0.0.31",
48
- "@riebeckite/diagnostics": "0.0.31",
49
- "@riebeckite/plugin-color-mode": "0.0.31",
50
- "@riebeckite/ui": "0.0.31"
47
+ "@riebeckite/core": "0.0.32",
48
+ "@riebeckite/diagnostics": "0.0.32",
49
+ "@riebeckite/plugin-color-mode": "0.0.32",
50
+ "@riebeckite/ui": "0.0.32",
51
+ "@riebeckite/spa": "0.0.32"
51
52
  },
52
53
  "devDependencies": {
53
54
  "tsx": "^4.22.4",
54
- "@riebeckite/test": "0.0.31"
55
+ "@riebeckite/test": "0.0.32"
55
56
  },
56
57
  "publishConfig": {
57
58
  "access": "public"