@pracht/vite-plugin 0.7.6 → 0.9.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/index.mjs CHANGED
@@ -1,14 +1,16 @@
1
- import { i as scanPagesDirectory, n as generatePagesManifestSource, o as createRouteLoaderHints } from "./pages-router-CStJY2Zp.mjs";
2
- import { createRequire } from "node:module";
1
+ import { c as createRouteStaticPathsHints, d as extensionGlob, f as normalizeAdditionalExtensions, i as scanPagesDirectory, l as DEFAULT_ROUTE_EXTENSIONS, n as generatePagesManifestSource, o as createRouteHeadHints, p as withAdditionalExtensions, s as createRouteLoaderHints, u as LEGACY_BARE_ROUTE_EXTENSIONS } from "./pages-router-KcCRkhnf.mjs";
2
+ import { createRequire, isBuiltin } from "node:module";
3
3
  import { preactSsrPrecompile } from "@pracht/preact-ssr-precompile";
4
4
  import preact from "@preact/preset-vite";
5
5
  import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
6
6
  import { dirname, extname, join, resolve } from "node:path";
7
7
  import { loadEnv, parseAst } from "vite";
8
+ import { PRACHT_GRAPH_ONLY_ENV } from "@pracht/core/server";
8
9
  import { CAPABILITY_SETTLED_EVENT, CAPABILITY_TRANSPORT_HEADER, CONFIRMATION_HEADER } from "@pracht/capabilities";
9
- import { extractCapabilityProjection, extractCapabilityRegistrations } from "@pracht/capabilities/static";
10
+ import { extractCapabilityProjection, extractCapabilityRegistrations, extractDefineAppObjectBody, scanTopLevelProperties } from "@pracht/capabilities/static";
10
11
  import { createNodeServerEntryModule } from "@pracht/adapter-node";
11
- import { resolveRegistryModule } from "@pracht/core";
12
+ import { Readable } from "node:stream";
13
+ import { applyDefaultSecurityHeaders, resolveRegistryModule } from "@pracht/core";
12
14
  //#region src/client-module-query.ts
13
15
  const CLIENT_MODULE_QUERY = "pracht-client";
14
16
  const PRACHT_CLIENT_MODULE_QUERY = `?${CLIENT_MODULE_QUERY}`;
@@ -27,7 +29,6 @@ function stripPrachtClientModuleQuery(id) {
27
29
  function getRolldownLang(id) {
28
30
  const path = stripPrachtClientModuleQuery(id).split("?")[0];
29
31
  if (/\.(c|m)?tsx$/i.test(path)) return "tsx";
30
- if (/\.tsrx$/i.test(path)) return "tsx";
31
32
  if (/\.(c|m)?ts$/i.test(path)) return "ts";
32
33
  if (/\.(c|m)?jsx$/i.test(path)) return "jsx";
33
34
  if (/\.mdx?$/i.test(path)) return "jsx";
@@ -1257,7 +1258,15 @@ const PRACHT_CAPABILITIES_MODULE_ID = "virtual:pracht/capabilities";
1257
1258
  const PRACHT_WEBMCP_MODULE_ID = "virtual:pracht/webmcp";
1258
1259
  const CLIENT_BROWSER_PATH = "/@pracht/client.js";
1259
1260
  const ISLANDS_CLIENT_BROWSER_PATH = "/@pracht/islands.js";
1260
- function readClientBuildAssets(root = process.cwd()) {
1261
+ /**
1262
+ * `base` prefixes every emitted asset URL. Vite normalizes it to leading and
1263
+ * trailing slashes; a CDN base (absolute or protocol-relative) is used as-is,
1264
+ * so the manifest paths still resolve.
1265
+ */
1266
+ function assetUrl(file, base) {
1267
+ return `${base}${file}`;
1268
+ }
1269
+ function readClientBuildAssets(root = process.cwd(), base = "/") {
1261
1270
  const manifestPath = ["dist/client/.vite/manifest.json", "dist/.vite/manifest.json"].map((candidate) => resolve(root, candidate)).find((candidate) => existsSync(candidate));
1262
1271
  if (!manifestPath) return {
1263
1272
  clientEntryUrl: null,
@@ -1275,22 +1284,22 @@ function readClientBuildAssets(root = process.cwd()) {
1275
1284
  if (!entry.src) continue;
1276
1285
  const deps = collectTransitiveDeps(manifest, key);
1277
1286
  const manifestKey = stripPrachtClientModuleQuery(entry.src);
1278
- if (deps.css.length > 0) cssManifest[manifestKey] = deps.css.map((f) => `/${f}`);
1279
- if (deps.js.length > 0) jsManifest[manifestKey] = deps.js.map((f) => `/${f}`);
1287
+ if (deps.css.length > 0) cssManifest[manifestKey] = deps.css.map((f) => assetUrl(f, base));
1288
+ if (deps.js.length > 0) jsManifest[manifestKey] = deps.js.map((f) => assetUrl(f, base));
1280
1289
  }
1281
- addEntryDeps(manifest, jsManifest, PRACHT_CLIENT_MODULE_ID, clientEntry);
1282
- addEntryDeps(manifest, jsManifest, PRACHT_ISLANDS_CLIENT_MODULE_ID, islandsEntry);
1290
+ addEntryDeps(manifest, jsManifest, PRACHT_CLIENT_MODULE_ID, clientEntry, base);
1291
+ addEntryDeps(manifest, jsManifest, PRACHT_ISLANDS_CLIENT_MODULE_ID, islandsEntry, base);
1283
1292
  return {
1284
- clientEntryUrl: clientEntry ? `/${clientEntry.file}` : null,
1285
- islandsEntryUrl: islandsEntry ? `/${islandsEntry.file}` : null,
1293
+ clientEntryUrl: clientEntry ? assetUrl(clientEntry.file, base) : null,
1294
+ islandsEntryUrl: islandsEntry ? assetUrl(islandsEntry.file, base) : null,
1286
1295
  cssManifest,
1287
1296
  jsManifest
1288
1297
  };
1289
1298
  }
1290
- function addEntryDeps(manifest, jsManifest, entryKey, entry) {
1299
+ function addEntryDeps(manifest, jsManifest, entryKey, entry, base) {
1291
1300
  if (!entry) return;
1292
1301
  const deps = collectTransitiveDeps(manifest, entryKey).js.filter((file) => file !== entry.file);
1293
- if (deps.length > 0) jsManifest[entryKey] = deps.map((file) => `/${file}`);
1302
+ if (deps.length > 0) jsManifest[entryKey] = deps.map((file) => assetUrl(file, base));
1294
1303
  }
1295
1304
  function collectTransitiveDeps(manifest, key) {
1296
1305
  const css = /* @__PURE__ */ new Set();
@@ -1349,6 +1358,7 @@ const DEFAULTS = {
1349
1358
  shellsDir: "/src/shells",
1350
1359
  apiDir: "/src/api",
1351
1360
  serverDir: "/src/server",
1361
+ additionalExtensions: [],
1352
1362
  islandsDir: "/src/islands",
1353
1363
  capabilitiesDir: "/src/capabilities",
1354
1364
  adapter: createDefaultNodeAdapter(),
@@ -1367,6 +1377,13 @@ function resolveOptions(options) {
1367
1377
  ...options
1368
1378
  };
1369
1379
  if (resolved.llmsTxt === void 0) resolved.llmsTxt = false;
1380
+ resolved.additionalExtensions = normalizeAdditionalExtensions(resolved.additionalExtensions);
1381
+ if (!new Set([
1382
+ "spa",
1383
+ "ssr",
1384
+ "ssg",
1385
+ "isg"
1386
+ ]).has(resolved.pagesDefaultRender)) throw new Error("pracht({ pagesDefaultRender }) expects \"spa\", \"ssr\", \"ssg\", or \"isg\".");
1370
1387
  if (!Number.isInteger(resolved.prerenderConcurrency) || resolved.prerenderConcurrency <= 0) throw new Error("pracht({ prerenderConcurrency }) expects a positive integer.");
1371
1388
  if (!Number.isInteger(resolved.maxBodySize) || resolved.maxBodySize <= 0) throw new Error("pracht({ maxBodySize }) expects a positive integer number of bytes.");
1372
1389
  validateBudgets(resolved.budgets);
@@ -1416,6 +1433,82 @@ function validateBudgets(budgets) {
1416
1433
  * rather than silently dropping an endpoint.
1417
1434
  */
1418
1435
  /**
1436
+ * Whether the app can reach the agent surface at all — registered capabilities
1437
+ * or a `defineApp({ agents })` config. Drives the `__PRACHT_AGENT_SURFACE__`
1438
+ * define, which lets the bundler drop the capability and Web Bot Auth runtimes
1439
+ * from the server bundle of apps that use neither.
1440
+ *
1441
+ * Deliberately one-sided: it only answers `false` when the manifest is readable
1442
+ * and provably free of both. An unreadable manifest, a parse failure, or any
1443
+ * spread inside the manifest file (which could carry registrations this
1444
+ * analyzer cannot see) answers `true`, so the runtime keeps deciding for
1445
+ * itself. Being wrong the other way would 404 a capability in production that
1446
+ * works in dev.
1447
+ */
1448
+ function hasAgentSurface(options = {}, root = process.cwd()) {
1449
+ const resolved = resolveOptions(options);
1450
+ if (resolved.pagesDir) return false;
1451
+ const appFileAbs = resolve(root, resolved.appFile.replace(/^\//, ""));
1452
+ let manifestSource;
1453
+ try {
1454
+ manifestSource = readFileSync(appFileAbs, "utf-8");
1455
+ } catch {
1456
+ return true;
1457
+ }
1458
+ const appBody = extractDefineAppObjectBody(manifestSource);
1459
+ if (appBody === null) return true;
1460
+ const properties = scanTopLevelProperties(appBody);
1461
+ if (properties.has("agents") || properties.has("capabilities")) return true;
1462
+ if (/\b(?:agents|capabilities)\b/.test(appBody)) return true;
1463
+ if (appBody.includes("...") || hasOpaqueTopLevelProperty(appBody)) return true;
1464
+ try {
1465
+ return extractCapabilityRegistrations(manifestSource).length > 0;
1466
+ } catch {
1467
+ return true;
1468
+ }
1469
+ }
1470
+ /** Whether an object literal body contains an opaque key at its top level. */
1471
+ function hasOpaqueTopLevelProperty(objectBody) {
1472
+ let braces = 0;
1473
+ let brackets = 0;
1474
+ let parentheses = 0;
1475
+ let expectingKey = true;
1476
+ for (let index = 0; index < objectBody.length; index += 1) {
1477
+ const char = objectBody[index];
1478
+ const next = objectBody[index + 1];
1479
+ if (char === "\"" || char === "'" || char === "`") {
1480
+ const quote = char;
1481
+ for (index += 1; index < objectBody.length; index += 1) if (objectBody[index] === "\\") index += 1;
1482
+ else if (objectBody[index] === quote) break;
1483
+ continue;
1484
+ }
1485
+ if (char === "/" && next === "/") {
1486
+ index = objectBody.indexOf("\n", index + 2);
1487
+ if (index === -1) break;
1488
+ continue;
1489
+ }
1490
+ if (char === "/" && next === "*") {
1491
+ const end = objectBody.indexOf("*/", index + 2);
1492
+ if (end === -1) return true;
1493
+ index = end + 1;
1494
+ continue;
1495
+ }
1496
+ if (char === "/") return true;
1497
+ const atTopLevel = braces === 0 && brackets === 0 && parentheses === 0;
1498
+ if (atTopLevel && expectingKey && char === "[") return true;
1499
+ if (atTopLevel && expectingKey && char === "\\") return true;
1500
+ if (atTopLevel && char === ":") expectingKey = false;
1501
+ if (atTopLevel && char === ",") expectingKey = true;
1502
+ if (char === "{") braces += 1;
1503
+ else if (char === "}") braces -= 1;
1504
+ else if (char === "[") brackets += 1;
1505
+ else if (char === "]") brackets -= 1;
1506
+ else if (char === "(") parentheses += 1;
1507
+ else if (char === ")") parentheses -= 1;
1508
+ }
1509
+ return false;
1510
+ }
1511
+ /**
1419
1512
  * Extract capability registrations (name → module path) from the app
1420
1513
  * manifest source and their exposure metadata from each capability source.
1421
1514
  * Pages-router apps have no manifest, so capabilities are manifest-mode only.
@@ -1489,7 +1582,7 @@ function extractCapabilityMetadata(name, file, source) {
1489
1582
  */
1490
1583
  function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions = {}) {
1491
1584
  const capabilities = extractCapabilities(options, buildOptions.root);
1492
- const endpoints = {};
1585
+ const endpoints = Object.create(null);
1493
1586
  for (const capability of capabilities) if (capability.httpPath) endpoints[capability.name] = {
1494
1587
  method: "POST",
1495
1588
  path: capability.httpPath,
@@ -1499,7 +1592,9 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
1499
1592
  "// Generated by @pracht/vite-plugin from the app manifest capability registrations.",
1500
1593
  "// Contains only http-exposed capability names, endpoints, and effects —",
1501
1594
  "// capability modules themselves are server-only and never reach the client graph.",
1502
- `const endpoints = ${JSON.stringify(endpoints)};`,
1595
+ "import { createUseCapability, withBase } from \"@pracht/core\";",
1596
+ "",
1597
+ `const endpoints = Object.assign(Object.create(null), JSON.parse(${JSON.stringify(JSON.stringify(endpoints))}));`,
1503
1598
  "",
1504
1599
  "export const capabilityEndpoints = endpoints;",
1505
1600
  "",
@@ -1508,10 +1603,12 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
1508
1603
  " try {",
1509
1604
  " const headers = new Headers(opts && opts.headers);",
1510
1605
  " headers.set(\"content-type\", \"application/json\");",
1511
- " if (opts && opts.confirm) {",
1606
+ " if (opts && opts.prepare) {",
1607
+ ` headers.delete(${JSON.stringify(CONFIRMATION_HEADER)});`,
1608
+ " } else if (opts && opts.confirm) {",
1512
1609
  ` headers.set(${JSON.stringify(CONFIRMATION_HEADER)}, opts.confirm);`,
1513
1610
  " }",
1514
- " response = await fetch(endpoint.path, {",
1611
+ " response = await fetch(withBase(endpoint.path), {",
1515
1612
  " method: endpoint.method,",
1516
1613
  " headers,",
1517
1614
  " body: JSON.stringify(input === undefined ? {} : input),",
@@ -1524,8 +1621,9 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
1524
1621
  " error: { code: \"network_error\", message: String((error && error.message) || error) },",
1525
1622
  " };",
1526
1623
  " }",
1624
+ " let result;",
1527
1625
  " try {",
1528
- " return await response.json();",
1626
+ " result = await response.json();",
1529
1627
  " } catch {",
1530
1628
  " return {",
1531
1629
  " ok: false,",
@@ -1535,6 +1633,23 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
1535
1633
  " },",
1536
1634
  " };",
1537
1635
  " }",
1636
+ " if (",
1637
+ " !result || typeof result !== \"object\" ||",
1638
+ " (result.ok !== true && result.ok !== false) ||",
1639
+ " (result.ok === true && !(\"data\" in result)) ||",
1640
+ " (result.ok === false &&",
1641
+ " (!result.error || typeof result.error !== \"object\" ||",
1642
+ " typeof result.error.code !== \"string\" || typeof result.error.message !== \"string\"))",
1643
+ " ) {",
1644
+ " return {",
1645
+ " ok: false,",
1646
+ " error: {",
1647
+ " code: \"invalid_response\",",
1648
+ " message: `Capability endpoint returned an invalid envelope (status ${response.status}).`,",
1649
+ " },",
1650
+ " };",
1651
+ " }",
1652
+ " return result;",
1538
1653
  "}",
1539
1654
  "",
1540
1655
  "export async function callCapability(name, input, opts) {",
@@ -1565,6 +1680,38 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
1565
1680
  " } catch {}",
1566
1681
  " return result;",
1567
1682
  "}",
1683
+ "",
1684
+ "// Nested client: dotted capability names become object paths, so",
1685
+ "// `capabilities.notes.search(input)` calls `callCapability(\"notes.search\", input)`.",
1686
+ "// Built from the same endpoint table, so there is one dispatch path.",
1687
+ "function buildCapabilityClient(names) {",
1688
+ " const root = Object.create(null);",
1689
+ " for (const name of names) {",
1690
+ " const segments = name.split(\".\");",
1691
+ " const leaf = segments.pop();",
1692
+ " let node = root;",
1693
+ " for (const segment of segments) {",
1694
+ " // A name that is both a namespace and a leaf (`a` plus `a.b`) would",
1695
+ " // collide; the namespace wins and the leaf stays reachable through",
1696
+ " // callCapability(). `pracht verify` reports the shadowed name.",
1697
+ " if (typeof node[segment] !== \"object\" || node[segment] === null) {",
1698
+ " node[segment] = Object.create(null);",
1699
+ " }",
1700
+ " node = node[segment];",
1701
+ " }",
1702
+ " if (typeof node[leaf] !== \"object\") {",
1703
+ " node[leaf] = (input, opts) => callCapability(name, input, opts);",
1704
+ " }",
1705
+ " }",
1706
+ " return root;",
1707
+ "}",
1708
+ "",
1709
+ "export const capabilities = /*@__PURE__*/ buildCapabilityClient(Object.keys(endpoints));",
1710
+ "",
1711
+ "// The hook's implementation lives in @pracht/core (typed and unit-tested);",
1712
+ "// only the app-specific dispatch is bound here, so every projection shares",
1713
+ "// one call path. Pure-annotated: apps that never call it pay nothing.",
1714
+ "export const useCapability = /*@__PURE__*/ createUseCapability(callCapability);",
1568
1715
  ""
1569
1716
  ].join("\n");
1570
1717
  }
@@ -1654,15 +1801,6 @@ function hasWebmcpCapabilities(options = {}, root = process.cwd()) {
1654
1801
  }
1655
1802
  //#endregion
1656
1803
  //#region src/plugin-codegen.ts
1657
- const ROUTE_MODULE_EXTENSIONS = new Set([
1658
- ".ts",
1659
- ".tsx",
1660
- ".js",
1661
- ".jsx",
1662
- ".md",
1663
- ".mdx",
1664
- ".tsrx"
1665
- ]);
1666
1804
  const NON_FULL_HYDRATION_RE = /hydration\s*:\s*["'](?:islands|none)["']/;
1667
1805
  const FULL_HYDRATION_RE = /hydration\s*:\s*["']full["']/;
1668
1806
  const PAGES_NON_FULL_HYDRATION_RE = /export\s+const\s+HYDRATION\s*=\s*["'](?:islands|none)["']/;
@@ -1681,7 +1819,7 @@ function findMatching(source, start, open, close) {
1681
1819
  }
1682
1820
  return -1;
1683
1821
  }
1684
- function scanFiles(dir, files) {
1822
+ function scanFiles(dir, files, extensions) {
1685
1823
  let entries;
1686
1824
  try {
1687
1825
  entries = readdirSync(dir);
@@ -1696,15 +1834,15 @@ function scanFiles(dir, files) {
1696
1834
  } catch {
1697
1835
  continue;
1698
1836
  }
1699
- if (stat.isDirectory()) scanFiles(abs, files);
1700
- else if (ROUTE_MODULE_EXTENSIONS.has(extname(entry))) files.push(abs);
1837
+ if (stat.isDirectory()) scanFiles(abs, files, extensions);
1838
+ else if (extensions.has(extname(entry))) files.push(abs);
1701
1839
  }
1702
1840
  }
1703
1841
  function createNonFullHydrationExcludes(resolved, root = process.cwd()) {
1704
1842
  const excludes = /* @__PURE__ */ new Set();
1705
1843
  if (resolved.pagesDir) {
1706
1844
  const files = [];
1707
- scanFiles(resolve(root, resolved.pagesDir.replace(/^\//, "")), files);
1845
+ scanFiles(resolve(root, resolved.pagesDir.replace(/^\//, "")), files, withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, resolved.additionalExtensions));
1708
1846
  for (const file of files) try {
1709
1847
  if (PAGES_NON_FULL_HYDRATION_RE.test(readFileSync(file, "utf-8"))) excludes.add(`!/${toPosixPath$1(file).replace(toPosixPath$1(root).replace(/\/$/, "") + "/", "")}`);
1710
1848
  } catch {}
@@ -1754,15 +1892,18 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
1754
1892
  const resolved = resolveOptions(options);
1755
1893
  const isPagesMode = !!resolved.pagesDir;
1756
1894
  const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
1895
+ const routeHeadHints = createRouteHeadHintsForVirtualModules(resolved, buildOptions.root);
1896
+ const routeStaticPathsHints = createRouteStaticPathsHintsForVirtualModules(resolved, buildOptions.root);
1757
1897
  const appImport = isPagesMode ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`;
1898
+ const bareRouteExtensions = [...withAdditionalExtensions(LEGACY_BARE_ROUTE_EXTENSIONS, resolved.additionalExtensions)];
1758
1899
  const dirPrefix = isPagesMode ? resolved.pagesDir : resolved.routesDir;
1759
1900
  const routeGlob = `${dirPrefix}/**/*.{ts,tsx,js,jsx,md,mdx}`;
1760
- const routeTsrxGlob = `${dirPrefix}/**/*.tsrx`;
1901
+ const additionalRouteGlob = `${dirPrefix}/**/*.${extensionGlob(bareRouteExtensions)}`;
1761
1902
  const routeExcludes = createNonFullHydrationExcludes(resolved, buildOptions.root);
1762
1903
  const routeGlobPattern = routeExcludes.length > 0 ? [routeGlob, ...routeExcludes] : routeGlob;
1763
- const routeTsrxGlobPattern = routeExcludes.length > 0 ? [routeTsrxGlob, ...routeExcludes] : routeTsrxGlob;
1904
+ const additionalRouteGlobPattern = additionalRouteGlob && routeExcludes.length > 0 ? [additionalRouteGlob, ...routeExcludes] : additionalRouteGlob;
1764
1905
  const shellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.{ts,tsx,js,jsx}` : `${resolved.shellsDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
1765
- const shellTsrxGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.tsrx` : `${resolved.shellsDir}/**/*.tsrx`;
1906
+ const additionalShellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.${extensionGlob(bareRouteExtensions)}` : `${resolved.shellsDir}/**/*.${extensionGlob(bareRouteExtensions)}`;
1766
1907
  const appFilePosix = resolved.appFile.replace(/\\/g, "/").replace(/^\.\//, "");
1767
1908
  const appDir = (appFilePosix.startsWith("/") ? appFilePosix : `/${appFilePosix}`).replace(/\/[^/]*$/, "") || "/";
1768
1909
  return [
@@ -1770,17 +1911,19 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
1770
1911
  appImport,
1771
1912
  "",
1772
1913
  `const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
1914
+ `const routeHeadHints = ${JSON.stringify(routeHeadHints)};`,
1915
+ `const routeStaticPathsHints = ${JSON.stringify(routeStaticPathsHints)};`,
1773
1916
  `const routeModules = {`,
1774
1917
  ` ...import.meta.glob(${JSON.stringify(routeGlobPattern)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`,
1775
- ` ...import.meta.glob(${JSON.stringify(routeTsrxGlobPattern)}),`,
1918
+ ` ...import.meta.glob(${JSON.stringify(additionalRouteGlobPattern)}),`,
1776
1919
  `};`,
1777
1920
  `const shellModules = {`,
1778
1921
  ` ...import.meta.glob(${JSON.stringify(shellGlob)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`,
1779
- ` ...import.meta.glob(${JSON.stringify(shellTsrxGlob)}),`,
1922
+ ` ...import.meta.glob(${JSON.stringify(additionalShellGlob)}),`,
1780
1923
  `};`,
1781
1924
  "",
1782
1925
  "const resolvedApp = resolveApp(app);",
1783
- "applyRouteLoaderHints(resolvedApp, routeLoaderHints);",
1926
+ "applyRouteHints(resolvedApp, routeLoaderHints, routeHeadHints, routeStaticPathsHints);",
1784
1927
  "",
1785
1928
  ...createApplyRouteLoaderHintsSource(),
1786
1929
  `const APP_DIR = ${JSON.stringify(appDir)};`,
@@ -1876,7 +2019,9 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
1876
2019
  const isPagesMode = !!resolved.pagesDir;
1877
2020
  const registrySource = createPrachtRegistryModuleSource(resolved);
1878
2021
  const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
1879
- const clientBuild = buildOptions.isBuild ? readClientBuildAssets(buildOptions.root) : {
2022
+ const routeHeadHints = createRouteHeadHintsForVirtualModules(resolved, buildOptions.root);
2023
+ const routeStaticPathsHints = createRouteStaticPathsHintsForVirtualModules(resolved, buildOptions.root);
2024
+ const clientBuild = buildOptions.isBuild ? readClientBuildAssets(buildOptions.root, buildOptions.base ?? "/") : {
1880
2025
  clientEntryUrl: null,
1881
2026
  islandsEntryUrl: null,
1882
2027
  cssManifest: {},
@@ -1884,10 +2029,14 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
1884
2029
  };
1885
2030
  const adapter = resolved.adapter;
1886
2031
  const llmsTxtConfig = resolveLlmsTxtConfig(resolved, buildOptions.root);
2032
+ const islandsBootstrapRequired = hasWebmcpCapabilities(resolved, buildOptions.root);
1887
2033
  let prachtImports = adapter?.serverImports ? adapter.serverImports + "\nimport { prerenderApp } from \"@pracht/core/server\";" : "import { resolveApp, resolveApiRoutes, prerenderApp } from \"@pracht/core/server\";";
1888
2034
  if (llmsTxtConfig) prachtImports += "\nimport { buildLlmsTxt } from \"@pracht/core/server\";";
1889
2035
  const appImport = isPagesMode ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`;
1890
- const islandsEntryUrl = buildOptions.isBuild ? clientBuild.islandsEntryUrl : ISLANDS_CLIENT_BROWSER_PATH;
2036
+ const devBase = buildOptions.base ?? "/";
2037
+ const withDevBase = (path) => devBase === "/" ? path : `${devBase}${path.slice(1)}`;
2038
+ const clientEntryUrl = buildOptions.isBuild ? clientBuild.clientEntryUrl : withDevBase(CLIENT_BROWSER_PATH);
2039
+ const islandsEntryUrl = buildOptions.isBuild ? clientBuild.islandsEntryUrl : withDevBase(ISLANDS_CLIENT_BROWSER_PATH);
1891
2040
  const islandsGlob = `${resolved.islandsDir}/**/*.{ts,tsx,js,jsx}`;
1892
2041
  const source = [
1893
2042
  prachtImports,
@@ -1895,6 +2044,8 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
1895
2044
  appImport,
1896
2045
  "",
1897
2046
  `const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
2047
+ `const routeHeadHints = ${JSON.stringify(routeHeadHints)};`,
2048
+ `const routeStaticPathsHints = ${JSON.stringify(routeStaticPathsHints)};`,
1898
2049
  ...createApplyRouteLoaderHintsSource(),
1899
2050
  registrySource,
1900
2051
  "",
@@ -1906,11 +2057,15 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
1906
2057
  "export const islandFiles = Object.keys(islandModules);",
1907
2058
  "",
1908
2059
  "export const resolvedApp = resolveApp(app);",
1909
- "applyRouteLoaderHints(resolvedApp, routeLoaderHints);",
2060
+ "applyRouteHints(resolvedApp, routeLoaderHints, routeHeadHints, routeStaticPathsHints);",
1910
2061
  `export const apiRoutes = resolveApiRoutes(Object.keys(apiModules), ${JSON.stringify(resolved.apiDir)});`,
1911
2062
  `export const buildTarget = ${JSON.stringify(adapter?.id ?? "node")};`,
1912
- `export const clientEntryUrl = ${JSON.stringify(clientBuild.clientEntryUrl ?? "/@pracht/client.js")};`,
2063
+ `export const staticTarget = ${JSON.stringify(adapter?.staticTarget === true)};`,
2064
+ `export const buildBase = ${JSON.stringify(buildOptions.base ?? "/")};`,
2065
+ `export const configuredBase = ${JSON.stringify(buildOptions.configuredBase)};`,
2066
+ `export const clientEntryUrl = ${JSON.stringify(clientEntryUrl ?? "/@pracht/client.js")};`,
1913
2067
  `export const islandsEntryUrl = ${JSON.stringify(islandsEntryUrl ?? null)};`,
2068
+ `export const islandsBootstrapRequired = ${JSON.stringify(islandsBootstrapRequired)};`,
1914
2069
  `export const cssManifest = ${JSON.stringify(clientBuild.cssManifest)};`,
1915
2070
  `export const jsManifest = ${JSON.stringify(clientBuild.jsManifest)};`,
1916
2071
  `export const prerenderConcurrency = ${JSON.stringify(resolved.prerenderConcurrency)};`,
@@ -1936,15 +2091,25 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
1936
2091
  */
1937
2092
  function createPrachtDevModuleSource(options = {}, buildOptions = {}) {
1938
2093
  const resolved = resolveOptions(options);
2094
+ const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
2095
+ const routeHeadHints = createRouteHeadHintsForVirtualModules(resolved, buildOptions.root);
2096
+ const routeStaticPathsHints = createRouteStaticPathsHintsForVirtualModules(resolved, buildOptions.root);
1939
2097
  return [
1940
2098
  "import { resolveApp, resolveApiRoutes } from \"@pracht/core/server\";",
1941
2099
  resolved.pagesDir ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`,
1942
2100
  "",
2101
+ `const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
2102
+ `const routeHeadHints = ${JSON.stringify(routeHeadHints)};`,
2103
+ `const routeStaticPathsHints = ${JSON.stringify(routeStaticPathsHints)};`,
2104
+ ...createApplyRouteLoaderHintsSource(),
1943
2105
  createPrachtRegistryModuleSource(resolved),
1944
2106
  "",
1945
2107
  "export const resolvedApp = resolveApp(app);",
2108
+ "applyRouteHints(resolvedApp, routeLoaderHints, routeHeadHints, routeStaticPathsHints);",
1946
2109
  `export const apiRoutes = resolveApiRoutes(Object.keys(apiModules), ${JSON.stringify(resolved.apiDir)});`,
1947
2110
  `export const buildTarget = ${JSON.stringify(resolved.adapter?.id ?? "node")};`,
2111
+ `export const staticTarget = ${JSON.stringify(resolved.adapter?.staticTarget === true)};`,
2112
+ `export const buildBase = ${JSON.stringify(buildOptions.base ?? "/")};`,
1948
2113
  ""
1949
2114
  ].join("\n");
1950
2115
  }
@@ -1964,11 +2129,12 @@ function resolveLlmsTxtConfig(resolved, root = process.cwd()) {
1964
2129
  if (description) config.description = description;
1965
2130
  if (resolved.llmsTxt.origin) config.origin = resolved.llmsTxt.origin;
1966
2131
  if (resolved.llmsTxt.include) config.include = resolved.llmsTxt.include;
2132
+ if (resolved.llmsTxt.exclude?.length) config.exclude = resolved.llmsTxt.exclude;
1967
2133
  return config;
1968
2134
  }
1969
2135
  function createApplyRouteLoaderHintsSource() {
1970
2136
  return [
1971
- "function applyRouteLoaderHints(resolvedApp, routeLoaderHints) {",
2137
+ "function applyRouteHints(resolvedApp, routeLoaderHints, routeHeadHints, routeStaticPathsHints) {",
1972
2138
  " for (const route of resolvedApp.routes) {",
1973
2139
  " const hint = routeLoaderHints[route.file];",
1974
2140
  " if (hint === true) {",
@@ -1976,14 +2142,52 @@ function createApplyRouteLoaderHintsSource() {
1976
2142
  " } else if (typeof route.hasLoader === 'undefined' && typeof hint === 'boolean') {",
1977
2143
  " route.hasLoader = hint;",
1978
2144
  " }",
2145
+ " const routeHeadHint = routeHeadHints[route.file];",
2146
+ " const shellHeadHint = route.shellFile ? routeHeadHints[route.shellFile] : undefined;",
2147
+ " const hasCompleteHeadHints = typeof routeHeadHint === 'boolean' &&",
2148
+ " (!route.shellFile || typeof shellHeadHint === 'boolean');",
2149
+ " if (routeHeadHint === true || shellHeadHint === true) {",
2150
+ " route.hasHead = true;",
2151
+ " } else if (typeof route.hasHead === 'undefined' && hasCompleteHeadHints) {",
2152
+ " route.hasHead = routeHeadHint === true || shellHeadHint === true;",
2153
+ " }",
2154
+ " // Only ever narrows to false, and only when the scan actually saw the",
2155
+ " // module: an unknown route file keeps today's conservative behavior.",
2156
+ " const staticPathsHint = routeStaticPathsHints[route.file];",
2157
+ " if (typeof route.hasStaticPaths === 'undefined' && typeof staticPathsHint === 'boolean') {",
2158
+ " route.hasStaticPaths = staticPathsHint;",
2159
+ " }",
1979
2160
  " }",
1980
2161
  "}",
1981
2162
  ""
1982
2163
  ];
1983
2164
  }
2165
+ function createRouteHeadHintsForVirtualModules(options, root = process.cwd()) {
2166
+ const appFileDir = dirname(resolve(root, options.appFile.slice(1)));
2167
+ const directories = options.pagesDir ? [[options.pagesDir, resolve(root, options.pagesDir.slice(1))]] : [[options.routesDir, resolve(root, options.routesDir.slice(1))], [options.shellsDir, resolve(root, options.shellsDir.slice(1))]];
2168
+ return Object.assign({}, ...directories.map(([prefix, directory]) => createRouteHeadHints(directory, {
2169
+ additionalExtensions: options.additionalExtensions,
2170
+ appFileDir,
2171
+ rootRelativePrefix: prefix
2172
+ })));
2173
+ }
2174
+ /**
2175
+ * `getStaticPaths()` presence per route file. Only routes matter — a shell
2176
+ * cannot enumerate paths — so unlike the head hints this skips the shells
2177
+ * directory.
2178
+ */
2179
+ function createRouteStaticPathsHintsForVirtualModules(options, root = process.cwd()) {
2180
+ const appFileDir = dirname(resolve(root, options.appFile.slice(1)));
2181
+ const routesPrefix = options.pagesDir || options.routesDir;
2182
+ return createRouteStaticPathsHints(resolve(root, routesPrefix.slice(1)), {
2183
+ additionalExtensions: options.additionalExtensions,
2184
+ appFileDir,
2185
+ rootRelativePrefix: routesPrefix
2186
+ });
2187
+ }
1984
2188
  function createRouteLoaderHintsForVirtualModules(options, root = process.cwd()) {
1985
2189
  if (options.pagesDir) {
1986
- const pages = scanPagesDirectory(resolve(root, options.pagesDir.slice(1)));
2190
+ const pages = scanPagesDirectory(resolve(root, options.pagesDir.slice(1)), options.additionalExtensions);
1987
2191
  const hints = {};
1988
2192
  for (const page of pages) {
1989
2193
  const key = `${options.pagesDir}/${page.relativePath.replace(/\\/g, "/")}`;
@@ -1993,28 +2197,31 @@ function createRouteLoaderHintsForVirtualModules(options, root = process.cwd())
1993
2197
  }
1994
2198
  const appFileDir = dirname(resolve(root, options.appFile.slice(1)));
1995
2199
  return createRouteLoaderHints(resolve(root, options.routesDir.slice(1)), {
2200
+ additionalExtensions: options.additionalExtensions,
1996
2201
  appFileDir,
1997
2202
  rootRelativePrefix: options.routesDir
1998
2203
  });
1999
2204
  }
2000
2205
  function createPrachtRegistryModuleSource(options = {}) {
2001
2206
  const resolved = resolveOptions(options);
2207
+ const apiGlobs = [`${resolved.apiDir}/**/*.{ts,js,tsx,jsx}`, `!${resolved.apiDir}/**/*.d.ts`];
2002
2208
  const isPagesMode = !!resolved.pagesDir;
2209
+ const bareRouteExtensions = [...withAdditionalExtensions(LEGACY_BARE_ROUTE_EXTENSIONS, resolved.additionalExtensions)];
2003
2210
  const routeGlob = isPagesMode ? `${resolved.pagesDir}/**/*.{ts,tsx,js,jsx,md,mdx}` : `${resolved.routesDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
2004
- const routeTsrxGlob = isPagesMode ? `${resolved.pagesDir}/**/*.tsrx` : `${resolved.routesDir}/**/*.tsrx`;
2211
+ const additionalRouteGlob = `${isPagesMode ? resolved.pagesDir : resolved.routesDir}/**/*.${extensionGlob(bareRouteExtensions)}`;
2005
2212
  const shellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.{ts,tsx,js,jsx}` : `${resolved.shellsDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
2006
- const shellTsrxGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.tsrx` : `${resolved.shellsDir}/**/*.tsrx`;
2213
+ const additionalShellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.${extensionGlob(bareRouteExtensions)}` : `${resolved.shellsDir}/**/*.${extensionGlob(bareRouteExtensions)}`;
2007
2214
  return [
2008
2215
  `export const routeModules = {`,
2009
2216
  ` ...import.meta.glob(${JSON.stringify(routeGlob)}),`,
2010
- ` ...import.meta.glob(${JSON.stringify(routeTsrxGlob)}),`,
2217
+ ` ...import.meta.glob(${JSON.stringify(additionalRouteGlob)}),`,
2011
2218
  `};`,
2012
2219
  `export const shellModules = {`,
2013
2220
  ` ...import.meta.glob(${JSON.stringify(shellGlob)}),`,
2014
- ` ...import.meta.glob(${JSON.stringify(shellTsrxGlob)}),`,
2221
+ ` ...import.meta.glob(${JSON.stringify(additionalShellGlob)}),`,
2015
2222
  `};`,
2016
2223
  `export const middlewareModules = import.meta.glob(${JSON.stringify(`${resolved.middlewareDir}/**/*.{ts,tsx,js,jsx}`)});`,
2017
- `export const apiModules = import.meta.glob(${JSON.stringify(`${resolved.apiDir}/**/*.{ts,js,tsx,jsx}`)});`,
2224
+ `export const apiModules = import.meta.glob(${JSON.stringify(apiGlobs)});`,
2018
2225
  `export const dataModules = import.meta.glob(${JSON.stringify(`${resolved.serverDir}/**/*.{ts,js,tsx,jsx}`)});`,
2019
2226
  `export const capabilityModules = import.meta.glob(${JSON.stringify(`${resolved.capabilitiesDir}/**/*.{ts,js,tsx,jsx}`)});`,
2020
2227
  "",
@@ -2035,13 +2242,15 @@ function clearPagesAppSourceCache() {
2035
2242
  function generatePagesAppInlineSource(options, root = process.cwd()) {
2036
2243
  const absPagesDir = resolve(root, options.pagesDir.slice(1));
2037
2244
  const cacheKey = JSON.stringify({
2245
+ additionalExtensions: options.additionalExtensions,
2038
2246
  absPagesDir,
2039
2247
  pagesDefaultRender: options.pagesDefaultRender,
2040
2248
  pagesDirPrefix: options.pagesDir
2041
2249
  });
2042
2250
  const cached = pagesAppSourceCache.get(cacheKey);
2043
2251
  if (cached) return cached;
2044
- const source = generatePagesManifestSource(scanPagesDirectory(absPagesDir), {
2252
+ const source = generatePagesManifestSource(scanPagesDirectory(absPagesDir, options.additionalExtensions), {
2253
+ additionalExtensions: options.additionalExtensions,
2045
2254
  pagesDir: absPagesDir,
2046
2255
  pagesDefaultRender: options.pagesDefaultRender,
2047
2256
  pagesDirPrefix: options.pagesDir
@@ -2056,8 +2265,46 @@ const DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
2056
2265
  const CSS_MODULE_URL_RE = /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
2057
2266
  const DEVTOOLS_JSON_PATH = "/_pracht.json";
2058
2267
  const LLMS_TXT_PATH = "/llms.txt";
2268
+ function isEventStreamContentType(contentType) {
2269
+ return contentType.split(";", 1)[0]?.trim().toLowerCase() === "text/event-stream";
2270
+ }
2271
+ /**
2272
+ * Adapter-owned dev servers can route every browser request through their
2273
+ * platform runtime before Vite's transform middleware gets a chance to serve
2274
+ * Pracht's stable virtual client entries. Serve those two entries at their
2275
+ * public, base-prefixed URLs while leaving every other request to the adapter.
2276
+ */
2277
+ function createOwnedDevEntryMiddleware(server) {
2278
+ const base = server.config.base || "/";
2279
+ return async (req, res, next) => {
2280
+ const method = (req.method ?? "GET").toUpperCase();
2281
+ if (method !== "GET" && method !== "HEAD") return next();
2282
+ const requestUrl = new URL(req.url ?? "/", "http://localhost");
2283
+ const pathname = base === "/" ? requestUrl.pathname : requestUrl.pathname.startsWith(base) ? `/${requestUrl.pathname.slice(base.length)}` : null;
2284
+ if (pathname !== "/@pracht/client.js" && pathname !== "/@pracht/islands.js") return next();
2285
+ try {
2286
+ const result = await server.transformRequest(`${pathname}${requestUrl.search}`);
2287
+ if (!result) return next();
2288
+ if (result.etag && req.headers["if-none-match"] === result.etag) {
2289
+ res.statusCode = 304;
2290
+ res.end();
2291
+ return;
2292
+ }
2293
+ res.statusCode = 200;
2294
+ res.setHeader("content-type", "text/javascript");
2295
+ res.setHeader("cache-control", "no-cache");
2296
+ if (result.etag) res.setHeader("etag", result.etag);
2297
+ for (const [name, value] of Object.entries(server.config.server.headers ?? {})) if (value !== void 0) res.setHeader(name, value);
2298
+ res.end(method === "HEAD" ? void 0 : result.code);
2299
+ } catch (error) {
2300
+ next(error);
2301
+ }
2302
+ };
2303
+ }
2059
2304
  function createDevSSRMiddleware(server, options = {}) {
2060
2305
  const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
2306
+ const devBase = server.config.base || "/";
2307
+ const withDevBase = (path) => devBase === "/" || !path.startsWith("/") ? path : `${devBase}${path.slice(1)}`;
2061
2308
  let warnedDevtoolsCollision = false;
2062
2309
  let warnedLlmsTxtCollision = false;
2063
2310
  if (options.llmsTxt && typeof server.config.publicDir === "string") {
@@ -2082,6 +2329,7 @@ function createDevSSRMiddleware(server, options = {}) {
2082
2329
  await serveDevtools(server, res, {
2083
2330
  apiRoutes: serverMod.apiRoutes ?? [],
2084
2331
  app: serverMod.resolvedApp,
2332
+ base: devBase,
2085
2333
  url,
2086
2334
  wantsJson: requestUrl.pathname === DEVTOOLS_JSON_PATH
2087
2335
  });
@@ -2095,14 +2343,17 @@ function createDevSSRMiddleware(server, options = {}) {
2095
2343
  const llmsTxt = await serverMod.generateLlmsTxt();
2096
2344
  res.statusCode = 200;
2097
2345
  res.setHeader("content-type", "text/plain; charset=utf-8");
2346
+ applyDefaultSecurityHeaders(new Headers()).forEach((value, key) => {
2347
+ res.setHeader(key, value);
2348
+ });
2098
2349
  res.end(llmsTxt);
2099
2350
  return;
2100
2351
  }
2101
2352
  if (shouldBypassDevSSR(requestUrl, req, routeMatchers)) return next();
2102
- if (isDevNotFoundRequest(requestUrl, req, routeMatchers)) return serveDevNotFound(server, res, next, url, requestUrl.pathname, routeMatchers);
2353
+ if (isDevNotFoundRequest(requestUrl, req, routeMatchers)) return serveDevNotFound(server, res, next, url, requestUrl.pathname, routeMatchers, devBase);
2103
2354
  let webRequest;
2104
2355
  try {
2105
- webRequest = await nodeToWebRequest(req, maxBodySize);
2356
+ webRequest = await nodeToWebRequest(req, maxBodySize, devBase);
2106
2357
  } catch (err) {
2107
2358
  if (err instanceof Error && err.message === "Request body too large") {
2108
2359
  res.statusCode = 413;
@@ -2117,15 +2368,36 @@ function createDevSSRMiddleware(server, options = {}) {
2117
2368
  registry: serverMod.registry,
2118
2369
  request: webRequest,
2119
2370
  debugErrors: true,
2120
- clientEntryUrl: CLIENT_BROWSER_PATH,
2371
+ clientEntryUrl: withDevBase(CLIENT_BROWSER_PATH),
2372
+ islandsEntryUrl: withDevBase(ISLANDS_CLIENT_BROWSER_PATH),
2373
+ islandsBootstrapRequired: serverMod.islandsBootstrapRequired === true,
2121
2374
  apiRoutes: serverMod.apiRoutes,
2122
2375
  timings
2123
2376
  });
2124
2377
  const responseContentType = response.headers.get("content-type") ?? "";
2125
2378
  if (response.status === 404 && !responseContentType.includes("application/json") && !routeMatchers.app?.notFound) return next();
2126
- const contentType = response.headers.get("content-type") ?? "text/html";
2379
+ const contentType = response.headers.get("content-type") ?? "";
2380
+ if (response.body && isEventStreamContentType(contentType)) {
2381
+ res.statusCode = response.status;
2382
+ response.headers.forEach((value, key) => {
2383
+ res.setHeader(key, value);
2384
+ });
2385
+ const source = Readable.fromWeb(response.body);
2386
+ if (res.destroyed || res.writableEnded) {
2387
+ source.destroy();
2388
+ return;
2389
+ }
2390
+ res.on("close", () => {
2391
+ if (!res.writableFinished) source.destroy();
2392
+ });
2393
+ source.on("error", () => {
2394
+ res.destroy();
2395
+ });
2396
+ source.pipe(res);
2397
+ return;
2398
+ }
2127
2399
  let body = await response.text();
2128
- if (contentType.includes("text/html")) body = await server.transformIndexHtml(url, body);
2400
+ if (contentType.includes("text/html")) body = await transformDevHtml(server, url, body, devBase);
2129
2401
  res.statusCode = response.status;
2130
2402
  response.headers.forEach((value, key) => {
2131
2403
  res.setHeader(key, value);
@@ -2134,7 +2406,56 @@ function createDevSSRMiddleware(server, options = {}) {
2134
2406
  if (serverTiming) res.setHeader("Server-Timing", serverTiming);
2135
2407
  res.end(body);
2136
2408
  } catch (error) {
2137
- await handleDevError(server, req, res, next, url, error);
2409
+ await handleDevError(server, req, res, next, url, error, devBase);
2410
+ }
2411
+ };
2412
+ }
2413
+ /**
2414
+ * Vite's HTML transform adds `config.base` to root-absolute asset attributes.
2415
+ * Pracht's runtime has already added it to URLs produced by `withBase()` — the
2416
+ * client entry, route-state preloads, image endpoints, and user-authored asset
2417
+ * URLs — while Vite-owned or module-graph URLs still need the transform. Hide
2418
+ * the already-based strings while the hooks run, then restore them afterward,
2419
+ * so each producer applies the deploy base exactly once.
2420
+ */
2421
+ async function transformDevHtml(server, url, html, base) {
2422
+ if (base === "/") return server.transformIndexHtml(url, html);
2423
+ const assetUrls = protectRootAbsoluteAssetAttributes(html);
2424
+ let placeholder = "https://pracht.invalid/__PRACHT_DEV_BASE_PLACEHOLDER__/";
2425
+ while (assetUrls.html.includes(placeholder)) placeholder += "_";
2426
+ const protectedHtml = assetUrls.html.replaceAll(base, placeholder);
2427
+ const transformedHtml = await server.transformIndexHtml(url, protectedHtml);
2428
+ return assetUrls.restore(transformedHtml.replaceAll(placeholder, base));
2429
+ }
2430
+ const HTML_ASSET_URL_ATTRIBUTE_RE = /(\s(?:src|href|xlink:href|data|srcset|imagesrcset|poster|content)\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/gi;
2431
+ /**
2432
+ * Runtime-rendered root-absolute asset URLs have already reached their final
2433
+ * public meaning: raw `/logo.svg` deliberately stays at the origin root,
2434
+ * while `withBase("/logo.svg")` is already under the deploy base. Vite's dev
2435
+ * HTML pass prefixes both, unlike production SSR, so hide complete attribute
2436
+ * values behind inert external URLs until that pass finishes.
2437
+ */
2438
+ function protectRootAbsoluteAssetAttributes(html) {
2439
+ let markerPrefix = "https://pracht.invalid/__PRACHT_DEV_ASSET_PLACEHOLDER__/";
2440
+ while (html.includes(markerPrefix)) markerPrefix += "_";
2441
+ const replacements = [];
2442
+ return {
2443
+ html: html.replace(HTML_ASSET_URL_ATTRIBUTE_RE, (match, prefix, doubleQuoted, singleQuoted, unquoted) => {
2444
+ const value = doubleQuoted ?? singleQuoted ?? unquoted ?? "";
2445
+ if (!(/(?:srcset)\s*=\s*$/i.test(prefix) ? /(?:^|,)\s*\/(?!\/)/.test(value) : /^\s*\/(?!\/)/.test(value))) return match;
2446
+ const marker = `${markerPrefix}${replacements.length}`;
2447
+ replacements.push({
2448
+ marker,
2449
+ value
2450
+ });
2451
+ if (doubleQuoted !== void 0) return `${prefix}"${marker}"`;
2452
+ if (singleQuoted !== void 0) return `${prefix}'${marker}'`;
2453
+ return `${prefix}${marker}`;
2454
+ }),
2455
+ restore(transformedHtml) {
2456
+ let restoredHtml = transformedHtml;
2457
+ for (const { marker, value } of replacements) restoredHtml = restoredHtml.replaceAll(marker, value);
2458
+ return restoredHtml;
2138
2459
  }
2139
2460
  };
2140
2461
  }
@@ -2146,7 +2467,7 @@ function createDevSSRMiddleware(server, options = {}) {
2146
2467
  * avoid a first-paint FOUC.
2147
2468
  */
2148
2469
  async function createDevCssManifest(server, options) {
2149
- const route = options.matchAppRoute(options.app, options.pathname)?.route ?? options.app.notFound;
2470
+ const route = options.pathname === null ? void 0 : options.matchAppRoute(options.app, options.pathname)?.route ?? options.app.notFound;
2150
2471
  if (!route) return {};
2151
2472
  const manifest = {};
2152
2473
  const modules = [...route.shellFile ? [{
@@ -2195,18 +2516,19 @@ function collectDevCssUrls(entry) {
2195
2516
  }
2196
2517
  return [...urls];
2197
2518
  }
2198
- function injectDevCssLinks(html, manifest) {
2519
+ function injectDevCssLinks(html, manifest, base = "/") {
2199
2520
  if (!html.includes("</head>")) return html;
2200
- const tags = [...new Set(Object.values(manifest).flat())].map((url) => escapeHtmlAttribute(url)).filter((escapedUrl) => !html.includes(`href="${escapedUrl}"`)).map((escapedUrl) => `<link rel="stylesheet" href="${escapedUrl}">`);
2521
+ const tags = [...new Set(Object.values(manifest).flat().map((url) => base === "/" || !url.startsWith("/") ? url : `${base}${url.slice(1)}`))].map((url) => escapeHtmlAttribute(url)).filter((escapedUrl) => !html.includes(`href="${escapedUrl}"`)).map((escapedUrl) => `<link rel="stylesheet" href="${escapedUrl}">`);
2201
2522
  if (tags.length === 0) return html;
2202
2523
  return html.replace("</head>", ` ${tags.join("\n ")}\n </head>`);
2203
2524
  }
2204
- async function injectDevCssForPath(server, path, html) {
2205
- return injectDevCssLinks(html, await createDevCssManifest(server, await resolveDevCssContextForPath(server, path)));
2525
+ async function injectDevCssForPath(server, path, html, options = {}) {
2526
+ return injectDevCssLinks(html, await createDevCssManifest(server, await resolveDevCssContextForPath(server, path, options)), server.config.base || "/");
2206
2527
  }
2207
- async function resolveDevCssContextForPath(server, path) {
2528
+ async function resolveDevCssContextForPath(server, path, options = {}) {
2208
2529
  const [framework, serverMod] = await Promise.all([server.ssrLoadModule("@pracht/core/server"), server.ssrLoadModule(PRACHT_DEV_MODULE_ID)]);
2209
- const pathname = new URL(path, "http://localhost").pathname;
2530
+ const publicPathname = new URL(path, "http://localhost").pathname;
2531
+ const pathname = options.basePathRetained ? framework.stripBase(publicPathname) : publicPathname;
2210
2532
  return {
2211
2533
  app: serverMod.resolvedApp,
2212
2534
  matchAppRoute: framework.matchAppRoute,
@@ -2228,7 +2550,7 @@ function createDevCssInjectionMiddleware(server) {
2228
2550
  next();
2229
2551
  return;
2230
2552
  }
2231
- const contextPromise = resolveDevCssContextForPath(server, req.url ?? "/").catch((error) => {
2553
+ const contextPromise = resolveDevCssContextForPath(server, req.url ?? "/", { basePathRetained: true }).catch((error) => {
2232
2554
  if (!warned) {
2233
2555
  warned = true;
2234
2556
  server.config.logger.warn(`[pracht] Could not discover development stylesheets: ${error instanceof Error ? error.message : String(error)}`);
@@ -2259,7 +2581,7 @@ function createDevCssInjectionMiddleware(server) {
2259
2581
  try {
2260
2582
  const context = await contextPromise;
2261
2583
  const manifest = context ? await createDevCssManifest(server, context) : null;
2262
- originalEnd(manifest ? injectDevCssLinks(body.toString("utf-8"), manifest) : body.toString("utf-8"), done);
2584
+ originalEnd(manifest ? injectDevCssLinks(body.toString("utf-8"), manifest, server.config.base || "/") : body.toString("utf-8"), done);
2263
2585
  } catch {
2264
2586
  originalEnd(body, done);
2265
2587
  }
@@ -2307,13 +2629,13 @@ async function serveDevtools(server, res, options) {
2307
2629
  res.end(JSON.stringify(graph, null, 2));
2308
2630
  return;
2309
2631
  }
2310
- let html = devtools.buildDevtoolsHtml(graph);
2632
+ let html = devtools.buildDevtoolsHtml(graph, { base: options.base });
2311
2633
  html = await server.transformIndexHtml(options.url, html);
2312
2634
  res.statusCode = 200;
2313
2635
  res.setHeader("content-type", "text/html; charset=utf-8");
2314
2636
  res.end(html);
2315
2637
  }
2316
- async function handleDevError(server, req, res, next, url, error) {
2638
+ async function handleDevError(server, req, res, next, url, error, base) {
2317
2639
  if (error instanceof Error) server.ssrFixStacktrace(error);
2318
2640
  if (req.headers["x-pracht-route-state-request"] === "1") {
2319
2641
  res.statusCode = 500;
@@ -2330,7 +2652,8 @@ async function handleDevError(server, req, res, next, url, error) {
2330
2652
  let html = buildErrorOverlayHtml({
2331
2653
  message: error instanceof Error ? error.message : String(error),
2332
2654
  stack: error instanceof Error ? error.stack : void 0,
2333
- root: server.config.root
2655
+ root: server.config.root,
2656
+ base
2334
2657
  });
2335
2658
  html = await server.transformIndexHtml(url, html);
2336
2659
  res.statusCode = 500;
@@ -2359,11 +2682,12 @@ function isDevNotFoundRequest(requestUrl, req, options = {}) {
2359
2682
  if (!accept.includes("text/html") && !accept.includes("application/xhtml+xml")) return false;
2360
2683
  return !matchesResolvedRoute(url.pathname, options);
2361
2684
  }
2362
- async function serveDevNotFound(server, res, next, url, pathname, options) {
2685
+ async function serveDevNotFound(server, res, next, url, pathname, options, base) {
2363
2686
  try {
2364
2687
  const { buildDevNotFoundHtml } = await server.ssrLoadModule("@pracht/core/dev-404");
2365
2688
  let html = buildDevNotFoundHtml({
2366
2689
  apiRoutes: options.apiRoutes.map((route) => ({ path: route.path })),
2690
+ base,
2367
2691
  requestedPath: pathname,
2368
2692
  routes: options.app.routes.map((route) => ({
2369
2693
  path: route.path,
@@ -2444,6 +2768,8 @@ const DEV_ASSET_EXTENSIONS = new Set([
2444
2768
  ".js",
2445
2769
  ".json",
2446
2770
  ".map",
2771
+ ".markdown",
2772
+ ".md",
2447
2773
  ".mjs",
2448
2774
  ".pdf",
2449
2775
  ".png",
@@ -2456,10 +2782,11 @@ const DEV_ASSET_EXTENSIONS = new Set([
2456
2782
  ".woff2",
2457
2783
  ".xml"
2458
2784
  ]);
2459
- async function nodeToWebRequest(req, maxBodySize) {
2785
+ async function nodeToWebRequest(req, maxBodySize, base = "/") {
2460
2786
  const protocol = "http";
2461
2787
  const host = req.headers.host ?? "localhost";
2462
- const url = new URL(req.url ?? "/", `${protocol}://${host}`);
2788
+ const path = req.url ?? "/";
2789
+ const url = new URL(base === "/" || !path.startsWith("/") ? path : `${base}${path.slice(1)}`, `${protocol}://${host}`);
2463
2790
  const method = req.method ?? "GET";
2464
2791
  const headers = new Headers();
2465
2792
  for (const [key, value] of Object.entries(req.headers)) {
@@ -2487,14 +2814,34 @@ async function nodeToWebRequest(req, maxBodySize) {
2487
2814
  }
2488
2815
  //#endregion
2489
2816
  //#region src/index.ts
2817
+ function reachesHeadBearingModule(modules, serverRoot, headHints) {
2818
+ const pending = [...modules];
2819
+ const seen = /* @__PURE__ */ new Set();
2820
+ while (pending.length > 0) {
2821
+ const module = pending.pop();
2822
+ if (!module || seen.has(module)) continue;
2823
+ seen.add(module);
2824
+ const modulePath = module.file ?? module.id?.split("?", 1)[0];
2825
+ if (modulePath) {
2826
+ const normalizedPath = toPosixPath(modulePath);
2827
+ if (headHints[normalizedPath.startsWith(serverRoot) ? normalizedPath.slice(serverRoot.length) : normalizedPath] === true) return true;
2828
+ }
2829
+ if (module.importers) pending.push(...module.importers);
2830
+ }
2831
+ return false;
2832
+ }
2490
2833
  function pracht(options = {}) {
2491
2834
  const resolved = resolveOptions(options);
2492
2835
  const isPagesMode = !!resolved.pagesDir;
2493
2836
  let root = process.cwd();
2494
2837
  let routeFileDirs = [];
2838
+ let clientRouteHeadHints = {};
2839
+ const routeFileExtensions = withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, resolved.additionalExtensions);
2495
2840
  let capabilityModulePaths = /* @__PURE__ */ new Set();
2496
2841
  if (isPagesMode && options.appFile) console.warn("[pracht] Both `pagesDir` and `appFile` are set. `pagesDir` takes precedence — `appFile` will be ignored.");
2497
2842
  let isBuild = false;
2843
+ let base = "/";
2844
+ let configuredBase;
2498
2845
  const prachtPlugin = {
2499
2846
  name: "pracht",
2500
2847
  enforce: "pre",
@@ -2502,13 +2849,20 @@ function pracht(options = {}) {
2502
2849
  const isEdge = resolved.adapter.edge === true;
2503
2850
  const isSSRBuild = env.isSsrBuild;
2504
2851
  const configRoot = _config.root ?? process.cwd();
2505
- const wantsIslandsEntry = env.command === "build" && !isSSRBuild && existsSync(resolveConfigPath(configRoot, resolved.islandsDir));
2852
+ const wantsIslandsEntry = env.command === "build" && !isSSRBuild && (existsSync(resolveConfigPath(configRoot, resolved.islandsDir)) || hasWebmcpCapabilities(resolved, configRoot));
2506
2853
  const envDir = _config.envDir ? resolve(configRoot, _config.envDir) : configRoot;
2507
2854
  const publicEnvDefine = JSON.stringify(loadEnv(env.mode, envDir, PUBLIC_ENV_PREFIX));
2855
+ const agentSurfaceDefine = env.command === "build" ? String(hasAgentSurface(resolved, configRoot)) : "true";
2856
+ const staticTargetDefine = String(env.command === "build" && resolved.adapter.staticTarget === true);
2508
2857
  return {
2509
2858
  appType: "custom",
2510
2859
  envPrefix: ["VITE_", PUBLIC_ENV_PREFIX],
2511
- define: { __PRACHT_PUBLIC_ENV__: publicEnvDefine },
2860
+ resolve: { dedupe: PREACT_DEDUPE },
2861
+ define: {
2862
+ __PRACHT_PUBLIC_ENV__: publicEnvDefine,
2863
+ __PRACHT_AGENT_SURFACE__: agentSurfaceDefine,
2864
+ __PRACHT_STATIC_TARGET__: staticTargetDefine
2865
+ },
2512
2866
  ...isSSRBuild ? {} : { build: { rollupOptions: {
2513
2867
  ...wantsIslandsEntry ? { input: [PRACHT_ISLANDS_CLIENT_MODULE_ID] } : {},
2514
2868
  output: { manualChunks(id) {
@@ -2520,13 +2874,25 @@ function pracht(options = {}) {
2520
2874
  noExternal: true,
2521
2875
  target: "webworker"
2522
2876
  },
2877
+ environments: { ssr: { resolve: {
2878
+ conditions: [
2879
+ "worker",
2880
+ "module",
2881
+ "browser",
2882
+ "development|production"
2883
+ ],
2884
+ external: ["node:module"]
2885
+ } } },
2523
2886
  build: { rollupOptions: { external: [/^cloudflare:/] } }
2524
- } : {}
2887
+ } : {},
2888
+ ...!isEdge && isSSRBuild ? { ssr: { noExternal: [PRACHT_SSR_NO_EXTERNAL] } } : {}
2525
2889
  };
2526
2890
  },
2527
2891
  configResolved(config) {
2892
+ assertSafeRootAbsoluteDeployBase(config.base);
2528
2893
  root = config.root;
2529
2894
  isBuild = config.command === "build";
2895
+ base = config.base;
2530
2896
  routeFileDirs = computeRouteFileDirs(root, resolved);
2531
2897
  capabilityModulePaths = new Set(resolveCapabilityModulePaths(resolved, root).map(canonicalFilePath));
2532
2898
  },
@@ -2542,11 +2908,19 @@ function pracht(options = {}) {
2542
2908
  },
2543
2909
  load(id) {
2544
2910
  if (isIslandsClientModule(id)) return createPrachtIslandsClientModuleSource(resolved, { root });
2545
- if (isClientModule(id)) return createPrachtClientModuleSource(resolved, { root });
2546
- if (isDevModule(id)) return createPrachtDevModuleSource(resolved, { root });
2911
+ if (isClientModule(id)) {
2912
+ clientRouteHeadHints = createRouteHeadHintsForVirtualModules(resolved, root);
2913
+ return createPrachtClientModuleSource(resolved, { root });
2914
+ }
2915
+ if (isDevModule(id)) return createPrachtDevModuleSource(resolved, {
2916
+ root,
2917
+ base
2918
+ });
2547
2919
  if (isServerModule(id)) return createPrachtServerModuleSource(resolved, {
2548
2920
  root,
2549
- isBuild
2921
+ isBuild,
2922
+ base,
2923
+ configuredBase
2550
2924
  });
2551
2925
  if (isCapabilitiesModule(id)) return createPrachtCapabilitiesClientModuleSource(resolved, { root });
2552
2926
  if (isWebmcpModule(id)) return createPrachtWebmcpModuleSource(resolved, { root });
@@ -2565,6 +2939,7 @@ function pracht(options = {}) {
2565
2939
  configureServer(server) {
2566
2940
  if (isPagesMode) watchPagesDirectory(server, resolved, root);
2567
2941
  if (resolved.adapter.ownsDevServer) {
2942
+ server.middlewares.use(createOwnedDevEntryMiddleware(server));
2568
2943
  server.middlewares.use(createDevCssInjectionMiddleware(server));
2569
2944
  return;
2570
2945
  }
@@ -2583,14 +2958,29 @@ function pracht(options = {}) {
2583
2958
  return html;
2584
2959
  }
2585
2960
  },
2586
- handleHotUpdate({ file, server }) {
2961
+ handleHotUpdate({ file, modules = [], server }) {
2587
2962
  const serverRoot = toPosixPath(server.config.root);
2588
2963
  const normalizedFile = toPosixPath(file);
2589
2964
  const relative = normalizedFile.startsWith(serverRoot) ? normalizedFile.slice(serverRoot.length) : normalizedFile;
2965
+ const changesRouteHeadSource = isPagesMode ? relative.startsWith(resolved.pagesDir) : relative.startsWith(resolved.routesDir) || relative.startsWith(resolved.shellsDir);
2966
+ const changesRouteHeadDependency = reachesHeadBearingModule(modules, serverRoot, clientRouteHeadHints);
2967
+ let shouldReloadClientHead = changesRouteHeadDependency;
2968
+ let clientHeadModule;
2969
+ if (changesRouteHeadSource || changesRouteHeadDependency) clientHeadModule = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
2970
+ if (changesRouteHeadSource) {
2971
+ const previousHint = clientRouteHeadHints[relative];
2972
+ try {
2973
+ const nextHints = createRouteHeadHintsForVirtualModules(resolved, root);
2974
+ shouldReloadClientHead ||= previousHint === true || nextHints[relative] === true;
2975
+ clientRouteHeadHints = nextHints;
2976
+ } catch {
2977
+ shouldReloadClientHead = true;
2978
+ }
2979
+ } else if (changesRouteHeadDependency && clientHeadModule) server.moduleGraph.invalidateModule(clientHeadModule);
2590
2980
  if (isPagesMode && relative.startsWith(resolved.pagesDir)) {
2591
2981
  clearPagesAppSourceCache();
2592
2982
  invalidateVirtualModules(server);
2593
- sendServerOnlyFullReload(server, file);
2983
+ if (!sendServerOnlyFullReload(server, file) && shouldReloadClientHead && clientHeadModule) return [...new Set([...modules, clientHeadModule])];
2594
2984
  return;
2595
2985
  }
2596
2986
  if (!isPagesMode && relative === resolved.appFile) {
@@ -2610,7 +3000,7 @@ function pracht(options = {}) {
2610
3000
  if (serverMod) server.moduleGraph.invalidateModule(serverMod);
2611
3001
  const devMod = server.moduleGraph.getModuleById(PRACHT_DEV_MODULE_ID);
2612
3002
  if (devMod) server.moduleGraph.invalidateModule(devMod);
2613
- if (relative.startsWith(resolved.routesDir)) {
3003
+ if (relative.startsWith(resolved.routesDir) || relative.startsWith(resolved.shellsDir)) {
2614
3004
  const clientMod = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
2615
3005
  if (clientMod) server.moduleGraph.invalidateModule(clientMod);
2616
3006
  }
@@ -2628,7 +3018,16 @@ function pracht(options = {}) {
2628
3018
  if (capabilityMod) server.moduleGraph.invalidateModule(capabilityMod);
2629
3019
  }
2630
3020
  }
2631
- sendServerOnlyFullReload(server, file);
3021
+ if (!sendServerOnlyFullReload(server, file) && shouldReloadClientHead && clientHeadModule) return [...new Set([...modules, clientHeadModule])];
3022
+ }
3023
+ };
3024
+ const configuredBasePlugin = {
3025
+ name: "pracht:configured-base",
3026
+ config: {
3027
+ order: "post",
3028
+ handler(config) {
3029
+ configuredBase = typeof config.base === "string" ? config.base : void 0;
3030
+ }
2632
3031
  }
2633
3032
  };
2634
3033
  const clientModuleTransformPlugin = {
@@ -2636,7 +3035,7 @@ function pracht(options = {}) {
2636
3035
  enforce: "post",
2637
3036
  transform(code, id, transformOptions) {
2638
3037
  if (!transformOptions?.ssr && isCapabilityModule(id, capabilityModulePaths)) throw new Error(`[pracht] Capability module ${JSON.stringify(toPosixPath(id))} was imported by client code. Capability modules are server-only — their run() implementation and its imports would be bundled for every visitor. Call the capability instead: \`callCapability\`/\`capabilities\` from "virtual:pracht/capabilities" in the browser, or \`invokeCapability\` from "@pracht/core/server" in loaders, middleware, and API routes.`);
2639
- if (!(isPrachtClientModuleId(id) || !transformOptions?.ssr && isRouteOrShellFile(id, routeFileDirs))) return null;
3038
+ if (!(isPrachtClientModuleId(id) || !transformOptions?.ssr && isRouteOrShellFile(id, routeFileDirs, routeFileExtensions))) return null;
2640
3039
  const transformed = stripServerOnlyExportsForClient(code, id);
2641
3040
  if (transformed === code) return null;
2642
3041
  return {
@@ -2645,11 +3044,12 @@ function pracht(options = {}) {
2645
3044
  };
2646
3045
  }
2647
3046
  };
3047
+ const edgeRuntimeSafetyPlugin = resolved.adapter.edge ? createEdgeRuntimeSafetyPlugin() : null;
2648
3048
  const optimizeDepsEntriesPlugin = {
2649
3049
  name: "pracht:optimize-deps-entries",
2650
3050
  enforce: "post",
2651
3051
  config(config) {
2652
- return withPrachtOptimizeDepsEntries(config, createPrachtOptimizeDepsEntries(resolved), createPrachtOptimizeDepsInclude(config.root ?? process.cwd()));
3052
+ return withPrachtOptimizeDepsEntries(config, resolved, createPrachtOptimizeDepsInclude(config.root ?? process.cwd()));
2653
3053
  }
2654
3054
  };
2655
3055
  const precompilePlugin = resolved.precompileSsrJsx ? preactSsrPrecompile({
@@ -2660,14 +3060,99 @@ function pracht(options = {}) {
2660
3060
  ...precompilePlugin ? [precompilePlugin] : [],
2661
3061
  ...preact(),
2662
3062
  prachtPlugin,
3063
+ configuredBasePlugin,
2663
3064
  clientModuleTransformPlugin,
3065
+ ...edgeRuntimeSafetyPlugin ? [edgeRuntimeSafetyPlugin] : [],
2664
3066
  createEnvSafetyPlugin(resolved.envSafety)
2665
3067
  ];
2666
- const adapterPlugins = resolved.adapter.vitePlugins?.();
3068
+ const adapterPlugins = isGraphOnlyMode() ? resolved.adapter.graphVitePlugins?.() : resolved.adapter.vitePlugins?.();
2667
3069
  if (adapterPlugins?.length) plugins.push(...adapterPlugins);
2668
3070
  plugins.push(optimizeDepsEntriesPlugin);
2669
3071
  return plugins;
2670
3072
  }
3073
+ function assertSafeRootAbsoluteDeployBase(base) {
3074
+ if (typeof base !== "string" || !base.startsWith("/") || base.startsWith("//")) return;
3075
+ let safe = !base.includes("?") && !base.includes("#");
3076
+ if (safe) try {
3077
+ const segments = base.split("/");
3078
+ safe = segments.every((segment, index) => {
3079
+ if (segment === "" && index !== 0 && index !== segments.length - 1) return false;
3080
+ const decoded = decodeURIComponent(segment);
3081
+ if (decoded === "." || decoded === "..") return false;
3082
+ for (const character of decoded) {
3083
+ const codePoint = character.codePointAt(0);
3084
+ if (character === "/" || character === "\\" || codePoint === 0 || codePoint !== void 0 && (codePoint <= 31 || codePoint === 127)) return false;
3085
+ }
3086
+ return true;
3087
+ });
3088
+ } catch {
3089
+ safe = false;
3090
+ }
3091
+ if (!safe) throw new Error(`[pracht] Vite \`base\` is set to ${JSON.stringify(base)}, but root-absolute deploy bases must contain safe URL segments. Repeated slashes, malformed escapes, and segments that decode to a path separator, \`.\`, \`..\`, NUL, or another control character are not allowed.`);
3092
+ }
3093
+ function isGraphOnlyMode() {
3094
+ return process.env[PRACHT_GRAPH_ONLY_ENV] === "1";
3095
+ }
3096
+ function createEdgeRuntimeSafetyPlugin() {
3097
+ let isSsrBuild = false;
3098
+ return {
3099
+ name: "pracht:edge-runtime-safety",
3100
+ apply: "build",
3101
+ enforce: "post",
3102
+ configResolved(config) {
3103
+ isSsrBuild = !!config.build.ssr;
3104
+ },
3105
+ generateBundle(_options, bundle) {
3106
+ const consumer = this.environment?.config?.consumer;
3107
+ if (!(consumer ? consumer === "server" : isSsrBuild)) return;
3108
+ const survivors = [];
3109
+ for (const [fileName, output] of Object.entries(bundle)) {
3110
+ if (output.type !== "chunk") continue;
3111
+ for (const specifier of collectNodeBuiltinImports(this.parse(output.code))) survivors.push({
3112
+ chunk: fileName,
3113
+ specifier
3114
+ });
3115
+ }
3116
+ if (survivors.length === 0) return;
3117
+ this.error([
3118
+ "[pracht] Edge server bundle retains Node.js builtin imports that are unavailable at runtime:",
3119
+ ...survivors.map(({ chunk, specifier }) => ` - ${specifier} in ${chunk}`),
3120
+ "Remove the Node-only dependency or move that route to a Node deployment target."
3121
+ ].join("\n"));
3122
+ }
3123
+ };
3124
+ }
3125
+ function collectNodeBuiltinImports(program) {
3126
+ const imports = /* @__PURE__ */ new Set();
3127
+ function sourceValue(node) {
3128
+ if (!node || typeof node !== "object" || !("value" in node)) return null;
3129
+ return typeof node.value === "string" ? node.value : null;
3130
+ }
3131
+ function visit(node) {
3132
+ if (Array.isArray(node)) {
3133
+ for (const item of node) visit(item);
3134
+ return;
3135
+ }
3136
+ if (!node || typeof node !== "object") return;
3137
+ const record = node;
3138
+ const type = record.type;
3139
+ if (type === "ImportDeclaration" || type === "ExportAllDeclaration" || type === "ExportNamedDeclaration" || type === "ImportExpression") {
3140
+ const specifier = sourceValue(record.source);
3141
+ if (specifier && isBuiltin(specifier)) imports.add(specifier);
3142
+ } else if (type === "CallExpression") {
3143
+ const callee = record.callee;
3144
+ const isImport = callee?.type === "Import";
3145
+ const isRequire = callee?.type === "Identifier" && callee.name === "require";
3146
+ if (isImport || isRequire) {
3147
+ const specifier = sourceValue(record.arguments?.[0]);
3148
+ if (specifier && isBuiltin(specifier)) imports.add(specifier);
3149
+ }
3150
+ }
3151
+ for (const value of Object.values(record)) visit(value);
3152
+ }
3153
+ visit(program);
3154
+ return imports;
3155
+ }
2671
3156
  const MANIFEST_CORE_IMPORTS = new Set([
2672
3157
  "defineApp",
2673
3158
  "group",
@@ -2687,6 +3172,8 @@ const PRACHT_OPTIMIZE_DEPS_INCLUDE = [
2687
3172
  "@pracht/core/islands-client",
2688
3173
  "@pracht/core/manifest"
2689
3174
  ];
3175
+ const PREACT_DEDUPE = ["preact", "preact-render-to-string"];
3176
+ const PRACHT_SSR_NO_EXTERNAL = /^@pracht\//;
2690
3177
  function createPrachtOptimizeDepsInclude(root) {
2691
3178
  try {
2692
3179
  if (!toPosixPath(createRequire(join(root, "package.json")).resolve("@pracht/core/package.json")).includes("/node_modules/")) return [];
@@ -2695,8 +3182,9 @@ function createPrachtOptimizeDepsInclude(root) {
2695
3182
  return [];
2696
3183
  }
2697
3184
  }
2698
- function withPrachtOptimizeDepsEntries(config, prachtEntries, prachtInclude) {
2699
- const environments = Object.fromEntries(Object.entries(config.environments ?? {}).map(([name, environment]) => [name, { optimizeDeps: { entries: mergeOptimizeDepsEntries(environment.optimizeDeps?.entries, prachtEntries) } }]));
3185
+ function withPrachtOptimizeDepsEntries(config, resolved, prachtInclude) {
3186
+ const prachtEntries = createPrachtOptimizeDepsEntries(resolved, config.optimizeDeps?.extensions);
3187
+ const environments = Object.fromEntries(Object.entries(config.environments ?? {}).map(([name, environment]) => [name, { optimizeDeps: { entries: mergeOptimizeDepsEntries(environment.optimizeDeps?.entries, createPrachtOptimizeDepsEntries(resolved, environment.optimizeDeps?.extensions ?? config.optimizeDeps?.extensions)) } }]));
2700
3188
  return {
2701
3189
  optimizeDeps: {
2702
3190
  entries: mergeOptimizeDepsEntries(config.optimizeDeps?.entries, prachtEntries),
@@ -2705,13 +3193,30 @@ function withPrachtOptimizeDepsEntries(config, prachtEntries, prachtInclude) {
2705
3193
  ...Object.keys(environments).length > 0 ? { environments } : {}
2706
3194
  };
2707
3195
  }
2708
- function createPrachtOptimizeDepsEntries(resolved) {
3196
+ const VITE_SCANNABLE_ROUTE_EXTENSIONS = new Set([
3197
+ ".ts",
3198
+ ".tsx",
3199
+ ".js",
3200
+ ".jsx",
3201
+ ".mts",
3202
+ ".mjs",
3203
+ ".cts",
3204
+ ".cjs",
3205
+ ".vue",
3206
+ ".svelte",
3207
+ ".astro",
3208
+ ".imba"
3209
+ ]);
3210
+ function createPrachtOptimizeDepsEntries(resolved, optimizerExtensions) {
2709
3211
  const scriptExtensions = "{ts,tsx,js,jsx}";
2710
- const routeExtensions = "{ts,tsx,js,jsx,md,mdx,tsrx}";
3212
+ const explicitlyScannable = new Set(optimizerExtensions ?? []);
3213
+ const routeExtensions = extensionGlob([...new Set([...DEFAULT_ROUTE_EXTENSIONS, ...resolved.additionalExtensions])].filter((extension) => VITE_SCANNABLE_ROUTE_EXTENSIONS.has(extension) || explicitlyScannable.has(extension)));
3214
+ const apiDir = toOptimizeDepsEntry(resolved.apiDir);
3215
+ const apiEntries = [`${apiDir}/**/*.{ts,js,tsx,jsx}`, `!${apiDir}/**/*.d.ts`];
2711
3216
  const entries = resolved.pagesDir ? [
2712
3217
  `${toOptimizeDepsEntry(resolved.pagesDir)}/**/*.${routeExtensions}`,
2713
3218
  `${toOptimizeDepsEntry(resolved.middlewareDir)}/**/*.${scriptExtensions}`,
2714
- `${toOptimizeDepsEntry(resolved.apiDir)}/**/*.{ts,js,tsx,jsx}`,
3219
+ ...apiEntries,
2715
3220
  `${toOptimizeDepsEntry(resolved.serverDir)}/**/*.{ts,js,tsx,jsx}`,
2716
3221
  `${toOptimizeDepsEntry(resolved.islandsDir)}/**/*.${scriptExtensions}`
2717
3222
  ] : [
@@ -2719,7 +3224,7 @@ function createPrachtOptimizeDepsEntries(resolved) {
2719
3224
  `${toOptimizeDepsEntry(resolved.routesDir)}/**/*.${routeExtensions}`,
2720
3225
  `${toOptimizeDepsEntry(resolved.shellsDir)}/**/*.${routeExtensions}`,
2721
3226
  `${toOptimizeDepsEntry(resolved.middlewareDir)}/**/*.${scriptExtensions}`,
2722
- `${toOptimizeDepsEntry(resolved.apiDir)}/**/*.{ts,js,tsx,jsx}`,
3227
+ ...apiEntries,
2723
3228
  `${toOptimizeDepsEntry(resolved.serverDir)}/**/*.{ts,js,tsx,jsx}`,
2724
3229
  `${toOptimizeDepsEntry(resolved.islandsDir)}/**/*.${scriptExtensions}`,
2725
3230
  `${toOptimizeDepsEntry(resolved.capabilitiesDir)}/**/*.{ts,js,tsx,jsx}`
@@ -2755,15 +3260,6 @@ function invalidateVirtualModules(server) {
2755
3260
  if (serverMod) server.moduleGraph.invalidateModule(serverMod);
2756
3261
  if (devMod) server.moduleGraph.invalidateModule(devMod);
2757
3262
  }
2758
- const ROUTE_FILE_EXTENSIONS = new Set([
2759
- ".ts",
2760
- ".tsx",
2761
- ".js",
2762
- ".jsx",
2763
- ".md",
2764
- ".mdx",
2765
- ".tsrx"
2766
- ]);
2767
3263
  function computeRouteFileDirs(root, resolved) {
2768
3264
  return (resolved.pagesDir ? [resolved.pagesDir] : [resolved.routesDir, resolved.shellsDir]).map((dir) => canonicalFilePath(resolveConfigPath(root, dir))).map(withTrailingSep);
2769
3265
  }
@@ -2794,7 +3290,7 @@ function canonicalFilePath(path) {
2794
3290
  return toPosixPath(path);
2795
3291
  }
2796
3292
  }
2797
- function isRouteOrShellFile(id, dirs) {
3293
+ function isRouteOrShellFile(id, dirs, extensions) {
2798
3294
  if (dirs.length === 0) return false;
2799
3295
  const queryStart = id.indexOf("?");
2800
3296
  const path = queryStart === -1 ? id : id.slice(0, queryStart);
@@ -2802,7 +3298,7 @@ function isRouteOrShellFile(id, dirs) {
2802
3298
  const extIndex = path.lastIndexOf(".");
2803
3299
  if (extIndex === -1) return false;
2804
3300
  const ext = path.slice(extIndex);
2805
- if (!ROUTE_FILE_EXTENSIONS.has(ext)) return false;
3301
+ if (!extensions.has(ext)) return false;
2806
3302
  const normalized = toPosixPath(path);
2807
3303
  return dirs.some((dir) => normalized.startsWith(dir));
2808
3304
  }