@askrjs/cli 0.0.22 → 0.0.23

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.
@@ -64,6 +64,8 @@ interface SsgOutputBudgets {
64
64
  };
65
65
  }
66
66
  interface SsgOutputReportConfig {
67
+ /** Deployment pathname prefix stripped from root-absolute asset references. */
68
+ basePath?: string;
67
69
  budgets?: SsgOutputBudgets;
68
70
  /** Number of largest pages retained in the summary. Defaults to 20. */
69
71
  largestPages?: number;
package/dist/ssg.js CHANGED
@@ -404,7 +404,7 @@ function isReportableAsset(filePath) {
404
404
  async function removeSsgOutputReport(outputDir) {
405
405
  await fs.rm(path.join(outputDir, REPORT_PATH), { force: true });
406
406
  }
407
- function localReference(reference, documentPath) {
407
+ function localReference(reference, documentPath, basePath) {
408
408
  let resolved;
409
409
  try {
410
410
  const portableDocumentPath = documentPath.replaceAll("\\", "/");
@@ -416,11 +416,25 @@ function localReference(reference, documentPath) {
416
416
  }
417
417
  if (resolved.origin !== "https://askr.invalid") return void 0;
418
418
  try {
419
- return decodeURIComponent(resolved.pathname).replace(/^\/+/, "");
419
+ let pathname = decodeURIComponent(resolved.pathname);
420
+ if (basePath && reference.startsWith("/") && (pathname === basePath || pathname.startsWith(`${basePath}/`))) pathname = pathname.slice(basePath.length);
421
+ return pathname.replace(/^\/+/, "");
420
422
  } catch {
421
423
  return;
422
424
  }
423
425
  }
426
+ function deploymentBasePath(value) {
427
+ if (value === void 0 || value === "" || value === "/") return "";
428
+ if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//") || value.includes("?") || value.includes("#") || value.includes("\\")) throw new Error("outputReport.basePath must be an absolute URL pathname");
429
+ let decoded;
430
+ try {
431
+ decoded = decodeURIComponent(value);
432
+ } catch {
433
+ throw new Error("outputReport.basePath must use valid URL encoding");
434
+ }
435
+ if (decoded.split("/").some((segment) => segment === "." || segment === "..")) throw new Error("outputReport.basePath must not contain dot-segment traversal");
436
+ return decoded.replace(/\/+$/, "");
437
+ }
424
438
  function positiveCount(value, fallback, label) {
425
439
  const resolved = value ?? fallback;
426
440
  if (!Number.isSafeInteger(resolved) || resolved < 1) throw new Error(`${label} must be a positive integer`);
@@ -433,6 +447,7 @@ function validateByteBudget(budget, label) {
433
447
  }
434
448
  }
435
449
  function validateConfig(config) {
450
+ const basePath = deploymentBasePath(config.basePath);
436
451
  const budgets = config.budgets;
437
452
  validateByteBudget(budgets?.routes, "outputReport.budgets.routes");
438
453
  for (const [route, budget] of Object.entries(budgets?.routeOverrides ?? {})) if (budget !== false) validateByteBudget(budget, `route override ${route}`);
@@ -443,6 +458,7 @@ function validateConfig(config) {
443
458
  for (const [route, share] of hydrationShares) if (share !== void 0 && share !== false && (!Number.isFinite(share) || share < 0 || share > 1)) throw new Error(`hydration share for ${route} must be from 0 through 1`);
444
459
  positiveCount(config.largestPages, 20, "outputReport.largestPages");
445
460
  positiveCount(config.largestAssets, 20, "outputReport.largestAssets");
461
+ return basePath;
446
462
  }
447
463
  function budgetViolations(report, budgets = {}) {
448
464
  const violations = [];
@@ -474,7 +490,7 @@ function budgetViolations(report, budgets = {}) {
474
490
  return violations;
475
491
  }
476
492
  async function writeSsgOutputReport(outputDir, routes, inspections, config = {}) {
477
- validateConfig(config);
493
+ const basePath = validateConfig(config);
478
494
  const assets = [];
479
495
  for (const filePath of (await emittedFiles(outputDir)).filter(isReportableAsset)) {
480
496
  const measured = size(await fs.readFile(path.join(outputDir, filePath)));
@@ -495,7 +511,7 @@ async function writeSsgOutputReport(outputDir, routes, inspections, config = {})
495
511
  const referenced = (references, type) => {
496
512
  const resolved = [];
497
513
  for (const reference of references) {
498
- const localPath = localReference(reference, inspection.filePath);
514
+ const localPath = localReference(reference, inspection.filePath, basePath);
499
515
  if (!localPath) continue;
500
516
  const asset = assetMap.get(localPath);
501
517
  if (!asset) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askrjs/cli",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "Unified CLI for the Askr platform",
5
5
  "homepage": "https://github.com/askrjs/askr-cli#readme",
6
6
  "bugs": {