@pracht/vite-plugin 0.8.0 → 0.10.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,4 +1,4 @@
1
- import { i as scanPagesDirectory, n as generatePagesManifestSource, o as createRouteLoaderHints } from "./pages-router-BQlG21oC.mjs";
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
2
  import { createRequire, isBuiltin } from "node:module";
3
3
  import { preactSsrPrecompile } from "@pracht/preact-ssr-precompile";
4
4
  import preact from "@preact/preset-vite";
@@ -9,6 +9,7 @@ import { PRACHT_GRAPH_ONLY_ENV } from "@pracht/core/server";
9
9
  import { CAPABILITY_SETTLED_EVENT, CAPABILITY_TRANSPORT_HEADER, CONFIRMATION_HEADER } from "@pracht/capabilities";
10
10
  import { extractCapabilityProjection, extractCapabilityRegistrations, extractDefineAppObjectBody, scanTopLevelProperties } from "@pracht/capabilities/static";
11
11
  import { createNodeServerEntryModule } from "@pracht/adapter-node";
12
+ import { Readable } from "node:stream";
12
13
  import { applyDefaultSecurityHeaders, resolveRegistryModule } from "@pracht/core";
13
14
  //#region src/client-module-query.ts
14
15
  const CLIENT_MODULE_QUERY = "pracht-client";
@@ -28,7 +29,6 @@ function stripPrachtClientModuleQuery(id) {
28
29
  function getRolldownLang(id) {
29
30
  const path = stripPrachtClientModuleQuery(id).split("?")[0];
30
31
  if (/\.(c|m)?tsx$/i.test(path)) return "tsx";
31
- if (/\.tsrx$/i.test(path)) return "tsx";
32
32
  if (/\.(c|m)?ts$/i.test(path)) return "ts";
33
33
  if (/\.(c|m)?jsx$/i.test(path)) return "jsx";
34
34
  if (/\.mdx?$/i.test(path)) return "jsx";
@@ -1258,7 +1258,15 @@ const PRACHT_CAPABILITIES_MODULE_ID = "virtual:pracht/capabilities";
1258
1258
  const PRACHT_WEBMCP_MODULE_ID = "virtual:pracht/webmcp";
1259
1259
  const CLIENT_BROWSER_PATH = "/@pracht/client.js";
1260
1260
  const ISLANDS_CLIENT_BROWSER_PATH = "/@pracht/islands.js";
1261
- 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 = "/") {
1262
1270
  const manifestPath = ["dist/client/.vite/manifest.json", "dist/.vite/manifest.json"].map((candidate) => resolve(root, candidate)).find((candidate) => existsSync(candidate));
1263
1271
  if (!manifestPath) return {
1264
1272
  clientEntryUrl: null,
@@ -1276,22 +1284,22 @@ function readClientBuildAssets(root = process.cwd()) {
1276
1284
  if (!entry.src) continue;
1277
1285
  const deps = collectTransitiveDeps(manifest, key);
1278
1286
  const manifestKey = stripPrachtClientModuleQuery(entry.src);
1279
- if (deps.css.length > 0) cssManifest[manifestKey] = deps.css.map((f) => `/${f}`);
1280
- 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));
1281
1289
  }
1282
- addEntryDeps(manifest, jsManifest, PRACHT_CLIENT_MODULE_ID, clientEntry);
1283
- 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);
1284
1292
  return {
1285
- clientEntryUrl: clientEntry ? `/${clientEntry.file}` : null,
1286
- islandsEntryUrl: islandsEntry ? `/${islandsEntry.file}` : null,
1293
+ clientEntryUrl: clientEntry ? assetUrl(clientEntry.file, base) : null,
1294
+ islandsEntryUrl: islandsEntry ? assetUrl(islandsEntry.file, base) : null,
1287
1295
  cssManifest,
1288
1296
  jsManifest
1289
1297
  };
1290
1298
  }
1291
- function addEntryDeps(manifest, jsManifest, entryKey, entry) {
1299
+ function addEntryDeps(manifest, jsManifest, entryKey, entry, base) {
1292
1300
  if (!entry) return;
1293
1301
  const deps = collectTransitiveDeps(manifest, entryKey).js.filter((file) => file !== entry.file);
1294
- if (deps.length > 0) jsManifest[entryKey] = deps.map((file) => `/${file}`);
1302
+ if (deps.length > 0) jsManifest[entryKey] = deps.map((file) => assetUrl(file, base));
1295
1303
  }
1296
1304
  function collectTransitiveDeps(manifest, key) {
1297
1305
  const css = /* @__PURE__ */ new Set();
@@ -1343,13 +1351,16 @@ function createDefaultNodeAdapter() {
1343
1351
  }
1344
1352
  //#endregion
1345
1353
  //#region src/plugin-options.ts
1354
+ const CLIENT_FEATURE_DEFAULTS = { prefetch: true };
1346
1355
  const DEFAULTS = {
1356
+ client: CLIENT_FEATURE_DEFAULTS,
1347
1357
  appFile: "/src/routes.ts",
1348
1358
  middlewareDir: "/src/middleware",
1349
1359
  routesDir: "/src/routes",
1350
1360
  shellsDir: "/src/shells",
1351
1361
  apiDir: "/src/api",
1352
1362
  serverDir: "/src/server",
1363
+ additionalExtensions: [],
1353
1364
  islandsDir: "/src/islands",
1354
1365
  capabilitiesDir: "/src/capabilities",
1355
1366
  adapter: createDefaultNodeAdapter(),
@@ -1368,6 +1379,8 @@ function resolveOptions(options) {
1368
1379
  ...options
1369
1380
  };
1370
1381
  if (resolved.llmsTxt === void 0) resolved.llmsTxt = false;
1382
+ resolved.client = resolveClientOptions(options.client);
1383
+ resolved.additionalExtensions = normalizeAdditionalExtensions(resolved.additionalExtensions);
1371
1384
  if (!new Set([
1372
1385
  "spa",
1373
1386
  "ssr",
@@ -1380,6 +1393,20 @@ function resolveOptions(options) {
1380
1393
  validateLlmsTxt(resolved.llmsTxt);
1381
1394
  return resolved;
1382
1395
  }
1396
+ function resolveClientOptions(client) {
1397
+ if (client === void 0) return CLIENT_FEATURE_DEFAULTS;
1398
+ if (typeof client !== "object" || client === null) throw new Error("pracht({ client }) expects an options object.");
1399
+ const resolved = { ...CLIENT_FEATURE_DEFAULTS };
1400
+ for (const key of Object.keys(CLIENT_FEATURE_DEFAULTS)) {
1401
+ const value = client[key];
1402
+ if (value === void 0) continue;
1403
+ if (typeof value !== "boolean") throw new Error(`pracht({ client: { ${key} } }) expects a boolean, got ${JSON.stringify(value)}.`);
1404
+ resolved[key] = value;
1405
+ }
1406
+ const unknown = Object.keys(client).filter((key) => !(key in CLIENT_FEATURE_DEFAULTS));
1407
+ if (unknown.length > 0) throw new Error(`pracht({ client }) does not accept ${unknown.map((key) => JSON.stringify(key)).join(", ")}. Known features: ${Object.keys(CLIENT_FEATURE_DEFAULTS).join(", ")}.`);
1408
+ return resolved;
1409
+ }
1383
1410
  const LLMS_TXT_SECTIONS = new Set([
1384
1411
  "pages",
1385
1412
  "api",
@@ -1582,7 +1609,7 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
1582
1609
  "// Generated by @pracht/vite-plugin from the app manifest capability registrations.",
1583
1610
  "// Contains only http-exposed capability names, endpoints, and effects —",
1584
1611
  "// capability modules themselves are server-only and never reach the client graph.",
1585
- "import { createUseCapability } from \"@pracht/core\";",
1612
+ "import { createUseCapability, ensureCapabilityRevalidation, withBase } from \"@pracht/core\";",
1586
1613
  "",
1587
1614
  `const endpoints = Object.assign(Object.create(null), JSON.parse(${JSON.stringify(JSON.stringify(endpoints))}));`,
1588
1615
  "",
@@ -1598,7 +1625,7 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
1598
1625
  " } else if (opts && opts.confirm) {",
1599
1626
  ` headers.set(${JSON.stringify(CONFIRMATION_HEADER)}, opts.confirm);`,
1600
1627
  " }",
1601
- " response = await fetch(endpoint.path, {",
1628
+ " response = await fetch(withBase(endpoint.path), {",
1602
1629
  " method: endpoint.method,",
1603
1630
  " headers,",
1604
1631
  " body: JSON.stringify(input === undefined ? {} : input),",
@@ -1643,6 +1670,7 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
1643
1670
  "}",
1644
1671
  "",
1645
1672
  "export async function callCapability(name, input, opts) {",
1673
+ " ensureCapabilityRevalidation();",
1646
1674
  " const endpoint = endpoints[name];",
1647
1675
  " if (!endpoint) {",
1648
1676
  " return {",
@@ -1791,15 +1819,6 @@ function hasWebmcpCapabilities(options = {}, root = process.cwd()) {
1791
1819
  }
1792
1820
  //#endregion
1793
1821
  //#region src/plugin-codegen.ts
1794
- const ROUTE_MODULE_EXTENSIONS = new Set([
1795
- ".ts",
1796
- ".tsx",
1797
- ".js",
1798
- ".jsx",
1799
- ".md",
1800
- ".mdx",
1801
- ".tsrx"
1802
- ]);
1803
1822
  const NON_FULL_HYDRATION_RE = /hydration\s*:\s*["'](?:islands|none)["']/;
1804
1823
  const FULL_HYDRATION_RE = /hydration\s*:\s*["']full["']/;
1805
1824
  const PAGES_NON_FULL_HYDRATION_RE = /export\s+const\s+HYDRATION\s*=\s*["'](?:islands|none)["']/;
@@ -1818,7 +1837,7 @@ function findMatching(source, start, open, close) {
1818
1837
  }
1819
1838
  return -1;
1820
1839
  }
1821
- function scanFiles(dir, files) {
1840
+ function scanFiles(dir, files, extensions) {
1822
1841
  let entries;
1823
1842
  try {
1824
1843
  entries = readdirSync(dir);
@@ -1833,15 +1852,15 @@ function scanFiles(dir, files) {
1833
1852
  } catch {
1834
1853
  continue;
1835
1854
  }
1836
- if (stat.isDirectory()) scanFiles(abs, files);
1837
- else if (ROUTE_MODULE_EXTENSIONS.has(extname(entry))) files.push(abs);
1855
+ if (stat.isDirectory()) scanFiles(abs, files, extensions);
1856
+ else if (extensions.has(extname(entry))) files.push(abs);
1838
1857
  }
1839
1858
  }
1840
1859
  function createNonFullHydrationExcludes(resolved, root = process.cwd()) {
1841
1860
  const excludes = /* @__PURE__ */ new Set();
1842
1861
  if (resolved.pagesDir) {
1843
1862
  const files = [];
1844
- scanFiles(resolve(root, resolved.pagesDir.replace(/^\//, "")), files);
1863
+ scanFiles(resolve(root, resolved.pagesDir.replace(/^\//, "")), files, withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, resolved.additionalExtensions));
1845
1864
  for (const file of files) try {
1846
1865
  if (PAGES_NON_FULL_HYDRATION_RE.test(readFileSync(file, "utf-8"))) excludes.add(`!/${toPosixPath$1(file).replace(toPosixPath$1(root).replace(/\/$/, "") + "/", "")}`);
1847
1866
  } catch {}
@@ -1891,15 +1910,18 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
1891
1910
  const resolved = resolveOptions(options);
1892
1911
  const isPagesMode = !!resolved.pagesDir;
1893
1912
  const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
1913
+ const routeHeadHints = createRouteHeadHintsForVirtualModules(resolved, buildOptions.root);
1914
+ const routeStaticPathsHints = createRouteStaticPathsHintsForVirtualModules(resolved, buildOptions.root);
1894
1915
  const appImport = isPagesMode ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`;
1916
+ const bareRouteExtensions = [...withAdditionalExtensions(LEGACY_BARE_ROUTE_EXTENSIONS, resolved.additionalExtensions)];
1895
1917
  const dirPrefix = isPagesMode ? resolved.pagesDir : resolved.routesDir;
1896
1918
  const routeGlob = `${dirPrefix}/**/*.{ts,tsx,js,jsx,md,mdx}`;
1897
- const routeTsrxGlob = `${dirPrefix}/**/*.tsrx`;
1919
+ const additionalRouteGlob = `${dirPrefix}/**/*.${extensionGlob(bareRouteExtensions)}`;
1898
1920
  const routeExcludes = createNonFullHydrationExcludes(resolved, buildOptions.root);
1899
1921
  const routeGlobPattern = routeExcludes.length > 0 ? [routeGlob, ...routeExcludes] : routeGlob;
1900
- const routeTsrxGlobPattern = routeExcludes.length > 0 ? [routeTsrxGlob, ...routeExcludes] : routeTsrxGlob;
1922
+ const additionalRouteGlobPattern = additionalRouteGlob && routeExcludes.length > 0 ? [additionalRouteGlob, ...routeExcludes] : additionalRouteGlob;
1901
1923
  const shellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.{ts,tsx,js,jsx}` : `${resolved.shellsDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
1902
- const shellTsrxGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.tsrx` : `${resolved.shellsDir}/**/*.tsrx`;
1924
+ const additionalShellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.${extensionGlob(bareRouteExtensions)}` : `${resolved.shellsDir}/**/*.${extensionGlob(bareRouteExtensions)}`;
1903
1925
  const appFilePosix = resolved.appFile.replace(/\\/g, "/").replace(/^\.\//, "");
1904
1926
  const appDir = (appFilePosix.startsWith("/") ? appFilePosix : `/${appFilePosix}`).replace(/\/[^/]*$/, "") || "/";
1905
1927
  return [
@@ -1907,17 +1929,19 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
1907
1929
  appImport,
1908
1930
  "",
1909
1931
  `const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
1932
+ `const routeHeadHints = ${JSON.stringify(routeHeadHints)};`,
1933
+ `const routeStaticPathsHints = ${JSON.stringify(routeStaticPathsHints)};`,
1910
1934
  `const routeModules = {`,
1911
1935
  ` ...import.meta.glob(${JSON.stringify(routeGlobPattern)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`,
1912
- ` ...import.meta.glob(${JSON.stringify(routeTsrxGlobPattern)}),`,
1936
+ ` ...import.meta.glob(${JSON.stringify(additionalRouteGlobPattern)}),`,
1913
1937
  `};`,
1914
1938
  `const shellModules = {`,
1915
1939
  ` ...import.meta.glob(${JSON.stringify(shellGlob)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`,
1916
- ` ...import.meta.glob(${JSON.stringify(shellTsrxGlob)}),`,
1940
+ ` ...import.meta.glob(${JSON.stringify(additionalShellGlob)}),`,
1917
1941
  `};`,
1918
1942
  "",
1919
1943
  "const resolvedApp = resolveApp(app);",
1920
- "applyRouteLoaderHints(resolvedApp, routeLoaderHints);",
1944
+ "applyRouteHints(resolvedApp, routeLoaderHints, routeHeadHints, routeStaticPathsHints);",
1921
1945
  "",
1922
1946
  ...createApplyRouteLoaderHintsSource(),
1923
1947
  `const APP_DIR = ${JSON.stringify(appDir)};`,
@@ -2013,7 +2037,9 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
2013
2037
  const isPagesMode = !!resolved.pagesDir;
2014
2038
  const registrySource = createPrachtRegistryModuleSource(resolved);
2015
2039
  const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
2016
- const clientBuild = buildOptions.isBuild ? readClientBuildAssets(buildOptions.root) : {
2040
+ const routeHeadHints = createRouteHeadHintsForVirtualModules(resolved, buildOptions.root);
2041
+ const routeStaticPathsHints = createRouteStaticPathsHintsForVirtualModules(resolved, buildOptions.root);
2042
+ const clientBuild = buildOptions.isBuild ? readClientBuildAssets(buildOptions.root, buildOptions.base ?? "/") : {
2017
2043
  clientEntryUrl: null,
2018
2044
  islandsEntryUrl: null,
2019
2045
  cssManifest: {},
@@ -2025,7 +2051,10 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
2025
2051
  let prachtImports = adapter?.serverImports ? adapter.serverImports + "\nimport { prerenderApp } from \"@pracht/core/server\";" : "import { resolveApp, resolveApiRoutes, prerenderApp } from \"@pracht/core/server\";";
2026
2052
  if (llmsTxtConfig) prachtImports += "\nimport { buildLlmsTxt } from \"@pracht/core/server\";";
2027
2053
  const appImport = isPagesMode ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`;
2028
- const islandsEntryUrl = buildOptions.isBuild ? clientBuild.islandsEntryUrl : ISLANDS_CLIENT_BROWSER_PATH;
2054
+ const devBase = buildOptions.base ?? "/";
2055
+ const withDevBase = (path) => devBase === "/" ? path : `${devBase}${path.slice(1)}`;
2056
+ const clientEntryUrl = buildOptions.isBuild ? clientBuild.clientEntryUrl : withDevBase(CLIENT_BROWSER_PATH);
2057
+ const islandsEntryUrl = buildOptions.isBuild ? clientBuild.islandsEntryUrl : withDevBase(ISLANDS_CLIENT_BROWSER_PATH);
2029
2058
  const islandsGlob = `${resolved.islandsDir}/**/*.{ts,tsx,js,jsx}`;
2030
2059
  const source = [
2031
2060
  prachtImports,
@@ -2033,6 +2062,8 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
2033
2062
  appImport,
2034
2063
  "",
2035
2064
  `const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
2065
+ `const routeHeadHints = ${JSON.stringify(routeHeadHints)};`,
2066
+ `const routeStaticPathsHints = ${JSON.stringify(routeStaticPathsHints)};`,
2036
2067
  ...createApplyRouteLoaderHintsSource(),
2037
2068
  registrySource,
2038
2069
  "",
@@ -2044,10 +2075,13 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
2044
2075
  "export const islandFiles = Object.keys(islandModules);",
2045
2076
  "",
2046
2077
  "export const resolvedApp = resolveApp(app);",
2047
- "applyRouteLoaderHints(resolvedApp, routeLoaderHints);",
2078
+ "applyRouteHints(resolvedApp, routeLoaderHints, routeHeadHints, routeStaticPathsHints);",
2048
2079
  `export const apiRoutes = resolveApiRoutes(Object.keys(apiModules), ${JSON.stringify(resolved.apiDir)});`,
2049
2080
  `export const buildTarget = ${JSON.stringify(adapter?.id ?? "node")};`,
2050
- `export const clientEntryUrl = ${JSON.stringify(clientBuild.clientEntryUrl ?? "/@pracht/client.js")};`,
2081
+ `export const staticTarget = ${JSON.stringify(adapter?.staticTarget === true)};`,
2082
+ `export const buildBase = ${JSON.stringify(buildOptions.base ?? "/")};`,
2083
+ `export const configuredBase = ${JSON.stringify(buildOptions.configuredBase)};`,
2084
+ `export const clientEntryUrl = ${JSON.stringify(clientEntryUrl ?? "/@pracht/client.js")};`,
2051
2085
  `export const islandsEntryUrl = ${JSON.stringify(islandsEntryUrl ?? null)};`,
2052
2086
  `export const islandsBootstrapRequired = ${JSON.stringify(islandsBootstrapRequired)};`,
2053
2087
  `export const cssManifest = ${JSON.stringify(clientBuild.cssManifest)};`,
@@ -2075,15 +2109,25 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
2075
2109
  */
2076
2110
  function createPrachtDevModuleSource(options = {}, buildOptions = {}) {
2077
2111
  const resolved = resolveOptions(options);
2112
+ const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
2113
+ const routeHeadHints = createRouteHeadHintsForVirtualModules(resolved, buildOptions.root);
2114
+ const routeStaticPathsHints = createRouteStaticPathsHintsForVirtualModules(resolved, buildOptions.root);
2078
2115
  return [
2079
2116
  "import { resolveApp, resolveApiRoutes } from \"@pracht/core/server\";",
2080
2117
  resolved.pagesDir ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`,
2081
2118
  "",
2119
+ `const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
2120
+ `const routeHeadHints = ${JSON.stringify(routeHeadHints)};`,
2121
+ `const routeStaticPathsHints = ${JSON.stringify(routeStaticPathsHints)};`,
2122
+ ...createApplyRouteLoaderHintsSource(),
2082
2123
  createPrachtRegistryModuleSource(resolved),
2083
2124
  "",
2084
2125
  "export const resolvedApp = resolveApp(app);",
2126
+ "applyRouteHints(resolvedApp, routeLoaderHints, routeHeadHints, routeStaticPathsHints);",
2085
2127
  `export const apiRoutes = resolveApiRoutes(Object.keys(apiModules), ${JSON.stringify(resolved.apiDir)});`,
2086
2128
  `export const buildTarget = ${JSON.stringify(resolved.adapter?.id ?? "node")};`,
2129
+ `export const staticTarget = ${JSON.stringify(resolved.adapter?.staticTarget === true)};`,
2130
+ `export const buildBase = ${JSON.stringify(buildOptions.base ?? "/")};`,
2087
2131
  ""
2088
2132
  ].join("\n");
2089
2133
  }
@@ -2108,7 +2152,7 @@ function resolveLlmsTxtConfig(resolved, root = process.cwd()) {
2108
2152
  }
2109
2153
  function createApplyRouteLoaderHintsSource() {
2110
2154
  return [
2111
- "function applyRouteLoaderHints(resolvedApp, routeLoaderHints) {",
2155
+ "function applyRouteHints(resolvedApp, routeLoaderHints, routeHeadHints, routeStaticPathsHints) {",
2112
2156
  " for (const route of resolvedApp.routes) {",
2113
2157
  " const hint = routeLoaderHints[route.file];",
2114
2158
  " if (hint === true) {",
@@ -2116,14 +2160,52 @@ function createApplyRouteLoaderHintsSource() {
2116
2160
  " } else if (typeof route.hasLoader === 'undefined' && typeof hint === 'boolean') {",
2117
2161
  " route.hasLoader = hint;",
2118
2162
  " }",
2163
+ " const routeHeadHint = routeHeadHints[route.file];",
2164
+ " const shellHeadHint = route.shellFile ? routeHeadHints[route.shellFile] : undefined;",
2165
+ " const hasCompleteHeadHints = typeof routeHeadHint === 'boolean' &&",
2166
+ " (!route.shellFile || typeof shellHeadHint === 'boolean');",
2167
+ " if (routeHeadHint === true || shellHeadHint === true) {",
2168
+ " route.hasHead = true;",
2169
+ " } else if (typeof route.hasHead === 'undefined' && hasCompleteHeadHints) {",
2170
+ " route.hasHead = routeHeadHint === true || shellHeadHint === true;",
2171
+ " }",
2172
+ " // Only ever narrows to false, and only when the scan actually saw the",
2173
+ " // module: an unknown route file keeps today's conservative behavior.",
2174
+ " const staticPathsHint = routeStaticPathsHints[route.file];",
2175
+ " if (typeof route.hasStaticPaths === 'undefined' && typeof staticPathsHint === 'boolean') {",
2176
+ " route.hasStaticPaths = staticPathsHint;",
2177
+ " }",
2119
2178
  " }",
2120
2179
  "}",
2121
2180
  ""
2122
2181
  ];
2123
2182
  }
2183
+ function createRouteHeadHintsForVirtualModules(options, root = process.cwd()) {
2184
+ const appFileDir = dirname(resolve(root, options.appFile.slice(1)));
2185
+ 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))]];
2186
+ return Object.assign({}, ...directories.map(([prefix, directory]) => createRouteHeadHints(directory, {
2187
+ additionalExtensions: options.additionalExtensions,
2188
+ appFileDir,
2189
+ rootRelativePrefix: prefix
2190
+ })));
2191
+ }
2192
+ /**
2193
+ * `getStaticPaths()` presence per route file. Only routes matter — a shell
2194
+ * cannot enumerate paths — so unlike the head hints this skips the shells
2195
+ * directory.
2196
+ */
2197
+ function createRouteStaticPathsHintsForVirtualModules(options, root = process.cwd()) {
2198
+ const appFileDir = dirname(resolve(root, options.appFile.slice(1)));
2199
+ const routesPrefix = options.pagesDir || options.routesDir;
2200
+ return createRouteStaticPathsHints(resolve(root, routesPrefix.slice(1)), {
2201
+ additionalExtensions: options.additionalExtensions,
2202
+ appFileDir,
2203
+ rootRelativePrefix: routesPrefix
2204
+ });
2205
+ }
2124
2206
  function createRouteLoaderHintsForVirtualModules(options, root = process.cwd()) {
2125
2207
  if (options.pagesDir) {
2126
- const pages = scanPagesDirectory(resolve(root, options.pagesDir.slice(1)));
2208
+ const pages = scanPagesDirectory(resolve(root, options.pagesDir.slice(1)), options.additionalExtensions);
2127
2209
  const hints = {};
2128
2210
  for (const page of pages) {
2129
2211
  const key = `${options.pagesDir}/${page.relativePath.replace(/\\/g, "/")}`;
@@ -2133,6 +2215,7 @@ function createRouteLoaderHintsForVirtualModules(options, root = process.cwd())
2133
2215
  }
2134
2216
  const appFileDir = dirname(resolve(root, options.appFile.slice(1)));
2135
2217
  return createRouteLoaderHints(resolve(root, options.routesDir.slice(1)), {
2218
+ additionalExtensions: options.additionalExtensions,
2136
2219
  appFileDir,
2137
2220
  rootRelativePrefix: options.routesDir
2138
2221
  });
@@ -2141,18 +2224,19 @@ function createPrachtRegistryModuleSource(options = {}) {
2141
2224
  const resolved = resolveOptions(options);
2142
2225
  const apiGlobs = [`${resolved.apiDir}/**/*.{ts,js,tsx,jsx}`, `!${resolved.apiDir}/**/*.d.ts`];
2143
2226
  const isPagesMode = !!resolved.pagesDir;
2227
+ const bareRouteExtensions = [...withAdditionalExtensions(LEGACY_BARE_ROUTE_EXTENSIONS, resolved.additionalExtensions)];
2144
2228
  const routeGlob = isPagesMode ? `${resolved.pagesDir}/**/*.{ts,tsx,js,jsx,md,mdx}` : `${resolved.routesDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
2145
- const routeTsrxGlob = isPagesMode ? `${resolved.pagesDir}/**/*.tsrx` : `${resolved.routesDir}/**/*.tsrx`;
2229
+ const additionalRouteGlob = `${isPagesMode ? resolved.pagesDir : resolved.routesDir}/**/*.${extensionGlob(bareRouteExtensions)}`;
2146
2230
  const shellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.{ts,tsx,js,jsx}` : `${resolved.shellsDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
2147
- const shellTsrxGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.tsrx` : `${resolved.shellsDir}/**/*.tsrx`;
2231
+ const additionalShellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.${extensionGlob(bareRouteExtensions)}` : `${resolved.shellsDir}/**/*.${extensionGlob(bareRouteExtensions)}`;
2148
2232
  return [
2149
2233
  `export const routeModules = {`,
2150
2234
  ` ...import.meta.glob(${JSON.stringify(routeGlob)}),`,
2151
- ` ...import.meta.glob(${JSON.stringify(routeTsrxGlob)}),`,
2235
+ ` ...import.meta.glob(${JSON.stringify(additionalRouteGlob)}),`,
2152
2236
  `};`,
2153
2237
  `export const shellModules = {`,
2154
2238
  ` ...import.meta.glob(${JSON.stringify(shellGlob)}),`,
2155
- ` ...import.meta.glob(${JSON.stringify(shellTsrxGlob)}),`,
2239
+ ` ...import.meta.glob(${JSON.stringify(additionalShellGlob)}),`,
2156
2240
  `};`,
2157
2241
  `export const middlewareModules = import.meta.glob(${JSON.stringify(`${resolved.middlewareDir}/**/*.{ts,tsx,js,jsx}`)});`,
2158
2242
  `export const apiModules = import.meta.glob(${JSON.stringify(apiGlobs)});`,
@@ -2176,13 +2260,15 @@ function clearPagesAppSourceCache() {
2176
2260
  function generatePagesAppInlineSource(options, root = process.cwd()) {
2177
2261
  const absPagesDir = resolve(root, options.pagesDir.slice(1));
2178
2262
  const cacheKey = JSON.stringify({
2263
+ additionalExtensions: options.additionalExtensions,
2179
2264
  absPagesDir,
2180
2265
  pagesDefaultRender: options.pagesDefaultRender,
2181
2266
  pagesDirPrefix: options.pagesDir
2182
2267
  });
2183
2268
  const cached = pagesAppSourceCache.get(cacheKey);
2184
2269
  if (cached) return cached;
2185
- const source = generatePagesManifestSource(scanPagesDirectory(absPagesDir), {
2270
+ const source = generatePagesManifestSource(scanPagesDirectory(absPagesDir, options.additionalExtensions), {
2271
+ additionalExtensions: options.additionalExtensions,
2186
2272
  pagesDir: absPagesDir,
2187
2273
  pagesDefaultRender: options.pagesDefaultRender,
2188
2274
  pagesDirPrefix: options.pagesDir
@@ -2197,8 +2283,46 @@ const DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
2197
2283
  const CSS_MODULE_URL_RE = /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
2198
2284
  const DEVTOOLS_JSON_PATH = "/_pracht.json";
2199
2285
  const LLMS_TXT_PATH = "/llms.txt";
2286
+ function isEventStreamContentType(contentType) {
2287
+ return contentType.split(";", 1)[0]?.trim().toLowerCase() === "text/event-stream";
2288
+ }
2289
+ /**
2290
+ * Adapter-owned dev servers can route every browser request through their
2291
+ * platform runtime before Vite's transform middleware gets a chance to serve
2292
+ * Pracht's stable virtual client entries. Serve those two entries at their
2293
+ * public, base-prefixed URLs while leaving every other request to the adapter.
2294
+ */
2295
+ function createOwnedDevEntryMiddleware(server) {
2296
+ const base = server.config.base || "/";
2297
+ return async (req, res, next) => {
2298
+ const method = (req.method ?? "GET").toUpperCase();
2299
+ if (method !== "GET" && method !== "HEAD") return next();
2300
+ const requestUrl = new URL(req.url ?? "/", "http://localhost");
2301
+ const pathname = base === "/" ? requestUrl.pathname : requestUrl.pathname.startsWith(base) ? `/${requestUrl.pathname.slice(base.length)}` : null;
2302
+ if (pathname !== "/@pracht/client.js" && pathname !== "/@pracht/islands.js") return next();
2303
+ try {
2304
+ const result = await server.transformRequest(`${pathname}${requestUrl.search}`);
2305
+ if (!result) return next();
2306
+ if (result.etag && req.headers["if-none-match"] === result.etag) {
2307
+ res.statusCode = 304;
2308
+ res.end();
2309
+ return;
2310
+ }
2311
+ res.statusCode = 200;
2312
+ res.setHeader("content-type", "text/javascript");
2313
+ res.setHeader("cache-control", "no-cache");
2314
+ if (result.etag) res.setHeader("etag", result.etag);
2315
+ for (const [name, value] of Object.entries(server.config.server.headers ?? {})) if (value !== void 0) res.setHeader(name, value);
2316
+ res.end(method === "HEAD" ? void 0 : result.code);
2317
+ } catch (error) {
2318
+ next(error);
2319
+ }
2320
+ };
2321
+ }
2200
2322
  function createDevSSRMiddleware(server, options = {}) {
2201
2323
  const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
2324
+ const devBase = server.config.base || "/";
2325
+ const withDevBase = (path) => devBase === "/" || !path.startsWith("/") ? path : `${devBase}${path.slice(1)}`;
2202
2326
  let warnedDevtoolsCollision = false;
2203
2327
  let warnedLlmsTxtCollision = false;
2204
2328
  if (options.llmsTxt && typeof server.config.publicDir === "string") {
@@ -2223,6 +2347,7 @@ function createDevSSRMiddleware(server, options = {}) {
2223
2347
  await serveDevtools(server, res, {
2224
2348
  apiRoutes: serverMod.apiRoutes ?? [],
2225
2349
  app: serverMod.resolvedApp,
2350
+ base: devBase,
2226
2351
  url,
2227
2352
  wantsJson: requestUrl.pathname === DEVTOOLS_JSON_PATH
2228
2353
  });
@@ -2243,10 +2368,10 @@ function createDevSSRMiddleware(server, options = {}) {
2243
2368
  return;
2244
2369
  }
2245
2370
  if (shouldBypassDevSSR(requestUrl, req, routeMatchers)) return next();
2246
- if (isDevNotFoundRequest(requestUrl, req, routeMatchers)) return serveDevNotFound(server, res, next, url, requestUrl.pathname, routeMatchers);
2371
+ if (isDevNotFoundRequest(requestUrl, req, routeMatchers)) return serveDevNotFound(server, res, next, url, requestUrl.pathname, routeMatchers, devBase);
2247
2372
  let webRequest;
2248
2373
  try {
2249
- webRequest = await nodeToWebRequest(req, maxBodySize);
2374
+ webRequest = await nodeToWebRequest(req, maxBodySize, devBase);
2250
2375
  } catch (err) {
2251
2376
  if (err instanceof Error && err.message === "Request body too large") {
2252
2377
  res.statusCode = 413;
@@ -2261,8 +2386,8 @@ function createDevSSRMiddleware(server, options = {}) {
2261
2386
  registry: serverMod.registry,
2262
2387
  request: webRequest,
2263
2388
  debugErrors: true,
2264
- clientEntryUrl: CLIENT_BROWSER_PATH,
2265
- islandsEntryUrl: ISLANDS_CLIENT_BROWSER_PATH,
2389
+ clientEntryUrl: withDevBase(CLIENT_BROWSER_PATH),
2390
+ islandsEntryUrl: withDevBase(ISLANDS_CLIENT_BROWSER_PATH),
2266
2391
  islandsBootstrapRequired: serverMod.islandsBootstrapRequired === true,
2267
2392
  apiRoutes: serverMod.apiRoutes,
2268
2393
  timings
@@ -2270,8 +2395,27 @@ function createDevSSRMiddleware(server, options = {}) {
2270
2395
  const responseContentType = response.headers.get("content-type") ?? "";
2271
2396
  if (response.status === 404 && !responseContentType.includes("application/json") && !routeMatchers.app?.notFound) return next();
2272
2397
  const contentType = response.headers.get("content-type") ?? "";
2398
+ if (response.body && isEventStreamContentType(contentType)) {
2399
+ res.statusCode = response.status;
2400
+ response.headers.forEach((value, key) => {
2401
+ res.setHeader(key, value);
2402
+ });
2403
+ const source = Readable.fromWeb(response.body);
2404
+ if (res.destroyed || res.writableEnded) {
2405
+ source.destroy();
2406
+ return;
2407
+ }
2408
+ res.on("close", () => {
2409
+ if (!res.writableFinished) source.destroy();
2410
+ });
2411
+ source.on("error", () => {
2412
+ res.destroy();
2413
+ });
2414
+ source.pipe(res);
2415
+ return;
2416
+ }
2273
2417
  let body = await response.text();
2274
- if (contentType.includes("text/html")) body = await server.transformIndexHtml(url, body);
2418
+ if (contentType.includes("text/html")) body = await transformDevHtml(server, url, body, devBase);
2275
2419
  res.statusCode = response.status;
2276
2420
  response.headers.forEach((value, key) => {
2277
2421
  res.setHeader(key, value);
@@ -2280,7 +2424,56 @@ function createDevSSRMiddleware(server, options = {}) {
2280
2424
  if (serverTiming) res.setHeader("Server-Timing", serverTiming);
2281
2425
  res.end(body);
2282
2426
  } catch (error) {
2283
- await handleDevError(server, req, res, next, url, error);
2427
+ await handleDevError(server, req, res, next, url, error, devBase);
2428
+ }
2429
+ };
2430
+ }
2431
+ /**
2432
+ * Vite's HTML transform adds `config.base` to root-absolute asset attributes.
2433
+ * Pracht's runtime has already added it to URLs produced by `withBase()` — the
2434
+ * client entry, route-state preloads, image endpoints, and user-authored asset
2435
+ * URLs — while Vite-owned or module-graph URLs still need the transform. Hide
2436
+ * the already-based strings while the hooks run, then restore them afterward,
2437
+ * so each producer applies the deploy base exactly once.
2438
+ */
2439
+ async function transformDevHtml(server, url, html, base) {
2440
+ if (base === "/") return server.transformIndexHtml(url, html);
2441
+ const assetUrls = protectRootAbsoluteAssetAttributes(html);
2442
+ let placeholder = "https://pracht.invalid/__PRACHT_DEV_BASE_PLACEHOLDER__/";
2443
+ while (assetUrls.html.includes(placeholder)) placeholder += "_";
2444
+ const protectedHtml = assetUrls.html.replaceAll(base, placeholder);
2445
+ const transformedHtml = await server.transformIndexHtml(url, protectedHtml);
2446
+ return assetUrls.restore(transformedHtml.replaceAll(placeholder, base));
2447
+ }
2448
+ const HTML_ASSET_URL_ATTRIBUTE_RE = /(\s(?:src|href|xlink:href|data|srcset|imagesrcset|poster|content)\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/gi;
2449
+ /**
2450
+ * Runtime-rendered root-absolute asset URLs have already reached their final
2451
+ * public meaning: raw `/logo.svg` deliberately stays at the origin root,
2452
+ * while `withBase("/logo.svg")` is already under the deploy base. Vite's dev
2453
+ * HTML pass prefixes both, unlike production SSR, so hide complete attribute
2454
+ * values behind inert external URLs until that pass finishes.
2455
+ */
2456
+ function protectRootAbsoluteAssetAttributes(html) {
2457
+ let markerPrefix = "https://pracht.invalid/__PRACHT_DEV_ASSET_PLACEHOLDER__/";
2458
+ while (html.includes(markerPrefix)) markerPrefix += "_";
2459
+ const replacements = [];
2460
+ return {
2461
+ html: html.replace(HTML_ASSET_URL_ATTRIBUTE_RE, (match, prefix, doubleQuoted, singleQuoted, unquoted) => {
2462
+ const value = doubleQuoted ?? singleQuoted ?? unquoted ?? "";
2463
+ if (!(/(?:srcset)\s*=\s*$/i.test(prefix) ? /(?:^|,)\s*\/(?!\/)/.test(value) : /^\s*\/(?!\/)/.test(value))) return match;
2464
+ const marker = `${markerPrefix}${replacements.length}`;
2465
+ replacements.push({
2466
+ marker,
2467
+ value
2468
+ });
2469
+ if (doubleQuoted !== void 0) return `${prefix}"${marker}"`;
2470
+ if (singleQuoted !== void 0) return `${prefix}'${marker}'`;
2471
+ return `${prefix}${marker}`;
2472
+ }),
2473
+ restore(transformedHtml) {
2474
+ let restoredHtml = transformedHtml;
2475
+ for (const { marker, value } of replacements) restoredHtml = restoredHtml.replaceAll(marker, value);
2476
+ return restoredHtml;
2284
2477
  }
2285
2478
  };
2286
2479
  }
@@ -2292,7 +2485,7 @@ function createDevSSRMiddleware(server, options = {}) {
2292
2485
  * avoid a first-paint FOUC.
2293
2486
  */
2294
2487
  async function createDevCssManifest(server, options) {
2295
- const route = options.matchAppRoute(options.app, options.pathname)?.route ?? options.app.notFound;
2488
+ const route = options.pathname === null ? void 0 : options.matchAppRoute(options.app, options.pathname)?.route ?? options.app.notFound;
2296
2489
  if (!route) return {};
2297
2490
  const manifest = {};
2298
2491
  const modules = [...route.shellFile ? [{
@@ -2341,18 +2534,19 @@ function collectDevCssUrls(entry) {
2341
2534
  }
2342
2535
  return [...urls];
2343
2536
  }
2344
- function injectDevCssLinks(html, manifest) {
2537
+ function injectDevCssLinks(html, manifest, base = "/") {
2345
2538
  if (!html.includes("</head>")) return html;
2346
- 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}">`);
2539
+ 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}">`);
2347
2540
  if (tags.length === 0) return html;
2348
2541
  return html.replace("</head>", ` ${tags.join("\n ")}\n </head>`);
2349
2542
  }
2350
- async function injectDevCssForPath(server, path, html) {
2351
- return injectDevCssLinks(html, await createDevCssManifest(server, await resolveDevCssContextForPath(server, path)));
2543
+ async function injectDevCssForPath(server, path, html, options = {}) {
2544
+ return injectDevCssLinks(html, await createDevCssManifest(server, await resolveDevCssContextForPath(server, path, options)), server.config.base || "/");
2352
2545
  }
2353
- async function resolveDevCssContextForPath(server, path) {
2546
+ async function resolveDevCssContextForPath(server, path, options = {}) {
2354
2547
  const [framework, serverMod] = await Promise.all([server.ssrLoadModule("@pracht/core/server"), server.ssrLoadModule(PRACHT_DEV_MODULE_ID)]);
2355
- const pathname = new URL(path, "http://localhost").pathname;
2548
+ const publicPathname = new URL(path, "http://localhost").pathname;
2549
+ const pathname = options.basePathRetained ? framework.stripBase(publicPathname) : publicPathname;
2356
2550
  return {
2357
2551
  app: serverMod.resolvedApp,
2358
2552
  matchAppRoute: framework.matchAppRoute,
@@ -2374,7 +2568,7 @@ function createDevCssInjectionMiddleware(server) {
2374
2568
  next();
2375
2569
  return;
2376
2570
  }
2377
- const contextPromise = resolveDevCssContextForPath(server, req.url ?? "/").catch((error) => {
2571
+ const contextPromise = resolveDevCssContextForPath(server, req.url ?? "/", { basePathRetained: true }).catch((error) => {
2378
2572
  if (!warned) {
2379
2573
  warned = true;
2380
2574
  server.config.logger.warn(`[pracht] Could not discover development stylesheets: ${error instanceof Error ? error.message : String(error)}`);
@@ -2405,7 +2599,7 @@ function createDevCssInjectionMiddleware(server) {
2405
2599
  try {
2406
2600
  const context = await contextPromise;
2407
2601
  const manifest = context ? await createDevCssManifest(server, context) : null;
2408
- originalEnd(manifest ? injectDevCssLinks(body.toString("utf-8"), manifest) : body.toString("utf-8"), done);
2602
+ originalEnd(manifest ? injectDevCssLinks(body.toString("utf-8"), manifest, server.config.base || "/") : body.toString("utf-8"), done);
2409
2603
  } catch {
2410
2604
  originalEnd(body, done);
2411
2605
  }
@@ -2453,13 +2647,13 @@ async function serveDevtools(server, res, options) {
2453
2647
  res.end(JSON.stringify(graph, null, 2));
2454
2648
  return;
2455
2649
  }
2456
- let html = devtools.buildDevtoolsHtml(graph);
2650
+ let html = devtools.buildDevtoolsHtml(graph, { base: options.base });
2457
2651
  html = await server.transformIndexHtml(options.url, html);
2458
2652
  res.statusCode = 200;
2459
2653
  res.setHeader("content-type", "text/html; charset=utf-8");
2460
2654
  res.end(html);
2461
2655
  }
2462
- async function handleDevError(server, req, res, next, url, error) {
2656
+ async function handleDevError(server, req, res, next, url, error, base) {
2463
2657
  if (error instanceof Error) server.ssrFixStacktrace(error);
2464
2658
  if (req.headers["x-pracht-route-state-request"] === "1") {
2465
2659
  res.statusCode = 500;
@@ -2476,7 +2670,8 @@ async function handleDevError(server, req, res, next, url, error) {
2476
2670
  let html = buildErrorOverlayHtml({
2477
2671
  message: error instanceof Error ? error.message : String(error),
2478
2672
  stack: error instanceof Error ? error.stack : void 0,
2479
- root: server.config.root
2673
+ root: server.config.root,
2674
+ base
2480
2675
  });
2481
2676
  html = await server.transformIndexHtml(url, html);
2482
2677
  res.statusCode = 500;
@@ -2505,11 +2700,12 @@ function isDevNotFoundRequest(requestUrl, req, options = {}) {
2505
2700
  if (!accept.includes("text/html") && !accept.includes("application/xhtml+xml")) return false;
2506
2701
  return !matchesResolvedRoute(url.pathname, options);
2507
2702
  }
2508
- async function serveDevNotFound(server, res, next, url, pathname, options) {
2703
+ async function serveDevNotFound(server, res, next, url, pathname, options, base) {
2509
2704
  try {
2510
2705
  const { buildDevNotFoundHtml } = await server.ssrLoadModule("@pracht/core/dev-404");
2511
2706
  let html = buildDevNotFoundHtml({
2512
2707
  apiRoutes: options.apiRoutes.map((route) => ({ path: route.path })),
2708
+ base,
2513
2709
  requestedPath: pathname,
2514
2710
  routes: options.app.routes.map((route) => ({
2515
2711
  path: route.path,
@@ -2604,10 +2800,11 @@ const DEV_ASSET_EXTENSIONS = new Set([
2604
2800
  ".woff2",
2605
2801
  ".xml"
2606
2802
  ]);
2607
- async function nodeToWebRequest(req, maxBodySize) {
2803
+ async function nodeToWebRequest(req, maxBodySize, base = "/") {
2608
2804
  const protocol = "http";
2609
2805
  const host = req.headers.host ?? "localhost";
2610
- const url = new URL(req.url ?? "/", `${protocol}://${host}`);
2806
+ const path = req.url ?? "/";
2807
+ const url = new URL(base === "/" || !path.startsWith("/") ? path : `${base}${path.slice(1)}`, `${protocol}://${host}`);
2611
2808
  const method = req.method ?? "GET";
2612
2809
  const headers = new Headers();
2613
2810
  for (const [key, value] of Object.entries(req.headers)) {
@@ -2635,14 +2832,34 @@ async function nodeToWebRequest(req, maxBodySize) {
2635
2832
  }
2636
2833
  //#endregion
2637
2834
  //#region src/index.ts
2835
+ function reachesHeadBearingModule(modules, serverRoot, headHints) {
2836
+ const pending = [...modules];
2837
+ const seen = /* @__PURE__ */ new Set();
2838
+ while (pending.length > 0) {
2839
+ const module = pending.pop();
2840
+ if (!module || seen.has(module)) continue;
2841
+ seen.add(module);
2842
+ const modulePath = module.file ?? module.id?.split("?", 1)[0];
2843
+ if (modulePath) {
2844
+ const normalizedPath = toPosixPath(modulePath);
2845
+ if (headHints[normalizedPath.startsWith(serverRoot) ? normalizedPath.slice(serverRoot.length) : normalizedPath] === true) return true;
2846
+ }
2847
+ if (module.importers) pending.push(...module.importers);
2848
+ }
2849
+ return false;
2850
+ }
2638
2851
  function pracht(options = {}) {
2639
2852
  const resolved = resolveOptions(options);
2640
2853
  const isPagesMode = !!resolved.pagesDir;
2641
2854
  let root = process.cwd();
2642
2855
  let routeFileDirs = [];
2856
+ let clientRouteHeadHints = {};
2857
+ const routeFileExtensions = withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, resolved.additionalExtensions);
2643
2858
  let capabilityModulePaths = /* @__PURE__ */ new Set();
2644
2859
  if (isPagesMode && options.appFile) console.warn("[pracht] Both `pagesDir` and `appFile` are set. `pagesDir` takes precedence — `appFile` will be ignored.");
2645
2860
  let isBuild = false;
2861
+ let base = "/";
2862
+ let configuredBase;
2646
2863
  const prachtPlugin = {
2647
2864
  name: "pracht",
2648
2865
  enforce: "pre",
@@ -2654,13 +2871,17 @@ function pracht(options = {}) {
2654
2871
  const envDir = _config.envDir ? resolve(configRoot, _config.envDir) : configRoot;
2655
2872
  const publicEnvDefine = JSON.stringify(loadEnv(env.mode, envDir, PUBLIC_ENV_PREFIX));
2656
2873
  const agentSurfaceDefine = env.command === "build" ? String(hasAgentSurface(resolved, configRoot)) : "true";
2874
+ const staticTargetDefine = String(env.command === "build" && resolved.adapter.staticTarget === true);
2875
+ const clientFeatureDefines = { __PRACHT_CLIENT_PREFETCH__: String(resolved.client.prefetch) };
2657
2876
  return {
2658
2877
  appType: "custom",
2659
2878
  envPrefix: ["VITE_", PUBLIC_ENV_PREFIX],
2660
2879
  resolve: { dedupe: PREACT_DEDUPE },
2661
2880
  define: {
2662
2881
  __PRACHT_PUBLIC_ENV__: publicEnvDefine,
2663
- __PRACHT_AGENT_SURFACE__: agentSurfaceDefine
2882
+ __PRACHT_AGENT_SURFACE__: agentSurfaceDefine,
2883
+ __PRACHT_STATIC_TARGET__: staticTargetDefine,
2884
+ ...clientFeatureDefines
2664
2885
  },
2665
2886
  ...isSSRBuild ? {} : { build: { rollupOptions: {
2666
2887
  ...wantsIslandsEntry ? { input: [PRACHT_ISLANDS_CLIENT_MODULE_ID] } : {},
@@ -2683,12 +2904,15 @@ function pracht(options = {}) {
2683
2904
  external: ["node:module"]
2684
2905
  } } },
2685
2906
  build: { rollupOptions: { external: [/^cloudflare:/] } }
2686
- } : {}
2907
+ } : {},
2908
+ ...!isEdge && isSSRBuild || env.command === "serve" ? { ssr: { noExternal: [PRACHT_SSR_NO_EXTERNAL] } } : {}
2687
2909
  };
2688
2910
  },
2689
2911
  configResolved(config) {
2912
+ assertSafeRootAbsoluteDeployBase(config.base);
2690
2913
  root = config.root;
2691
2914
  isBuild = config.command === "build";
2915
+ base = config.base;
2692
2916
  routeFileDirs = computeRouteFileDirs(root, resolved);
2693
2917
  capabilityModulePaths = new Set(resolveCapabilityModulePaths(resolved, root).map(canonicalFilePath));
2694
2918
  },
@@ -2704,11 +2928,19 @@ function pracht(options = {}) {
2704
2928
  },
2705
2929
  load(id) {
2706
2930
  if (isIslandsClientModule(id)) return createPrachtIslandsClientModuleSource(resolved, { root });
2707
- if (isClientModule(id)) return createPrachtClientModuleSource(resolved, { root });
2708
- if (isDevModule(id)) return createPrachtDevModuleSource(resolved, { root });
2931
+ if (isClientModule(id)) {
2932
+ clientRouteHeadHints = createRouteHeadHintsForVirtualModules(resolved, root);
2933
+ return createPrachtClientModuleSource(resolved, { root });
2934
+ }
2935
+ if (isDevModule(id)) return createPrachtDevModuleSource(resolved, {
2936
+ root,
2937
+ base
2938
+ });
2709
2939
  if (isServerModule(id)) return createPrachtServerModuleSource(resolved, {
2710
2940
  root,
2711
- isBuild
2941
+ isBuild,
2942
+ base,
2943
+ configuredBase
2712
2944
  });
2713
2945
  if (isCapabilitiesModule(id)) return createPrachtCapabilitiesClientModuleSource(resolved, { root });
2714
2946
  if (isWebmcpModule(id)) return createPrachtWebmcpModuleSource(resolved, { root });
@@ -2727,6 +2959,7 @@ function pracht(options = {}) {
2727
2959
  configureServer(server) {
2728
2960
  if (isPagesMode) watchPagesDirectory(server, resolved, root);
2729
2961
  if (resolved.adapter.ownsDevServer) {
2962
+ server.middlewares.use(createOwnedDevEntryMiddleware(server));
2730
2963
  server.middlewares.use(createDevCssInjectionMiddleware(server));
2731
2964
  return;
2732
2965
  }
@@ -2745,14 +2978,29 @@ function pracht(options = {}) {
2745
2978
  return html;
2746
2979
  }
2747
2980
  },
2748
- handleHotUpdate({ file, server }) {
2981
+ handleHotUpdate({ file, modules = [], server }) {
2749
2982
  const serverRoot = toPosixPath(server.config.root);
2750
2983
  const normalizedFile = toPosixPath(file);
2751
2984
  const relative = normalizedFile.startsWith(serverRoot) ? normalizedFile.slice(serverRoot.length) : normalizedFile;
2985
+ const changesRouteHeadSource = isPagesMode ? relative.startsWith(resolved.pagesDir) : relative.startsWith(resolved.routesDir) || relative.startsWith(resolved.shellsDir);
2986
+ const changesRouteHeadDependency = reachesHeadBearingModule(modules, serverRoot, clientRouteHeadHints);
2987
+ let shouldReloadClientHead = changesRouteHeadDependency;
2988
+ let clientHeadModule;
2989
+ if (changesRouteHeadSource || changesRouteHeadDependency) clientHeadModule = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
2990
+ if (changesRouteHeadSource) {
2991
+ const previousHint = clientRouteHeadHints[relative];
2992
+ try {
2993
+ const nextHints = createRouteHeadHintsForVirtualModules(resolved, root);
2994
+ shouldReloadClientHead ||= previousHint === true || nextHints[relative] === true;
2995
+ clientRouteHeadHints = nextHints;
2996
+ } catch {
2997
+ shouldReloadClientHead = true;
2998
+ }
2999
+ } else if (changesRouteHeadDependency && clientHeadModule) server.moduleGraph.invalidateModule(clientHeadModule);
2752
3000
  if (isPagesMode && relative.startsWith(resolved.pagesDir)) {
2753
3001
  clearPagesAppSourceCache();
2754
3002
  invalidateVirtualModules(server);
2755
- sendServerOnlyFullReload(server, file);
3003
+ if (!sendServerOnlyFullReload(server, file) && shouldReloadClientHead && clientHeadModule) return [...new Set([...modules, clientHeadModule])];
2756
3004
  return;
2757
3005
  }
2758
3006
  if (!isPagesMode && relative === resolved.appFile) {
@@ -2772,7 +3020,7 @@ function pracht(options = {}) {
2772
3020
  if (serverMod) server.moduleGraph.invalidateModule(serverMod);
2773
3021
  const devMod = server.moduleGraph.getModuleById(PRACHT_DEV_MODULE_ID);
2774
3022
  if (devMod) server.moduleGraph.invalidateModule(devMod);
2775
- if (relative.startsWith(resolved.routesDir)) {
3023
+ if (relative.startsWith(resolved.routesDir) || relative.startsWith(resolved.shellsDir)) {
2776
3024
  const clientMod = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
2777
3025
  if (clientMod) server.moduleGraph.invalidateModule(clientMod);
2778
3026
  }
@@ -2790,7 +3038,16 @@ function pracht(options = {}) {
2790
3038
  if (capabilityMod) server.moduleGraph.invalidateModule(capabilityMod);
2791
3039
  }
2792
3040
  }
2793
- sendServerOnlyFullReload(server, file);
3041
+ if (!sendServerOnlyFullReload(server, file) && shouldReloadClientHead && clientHeadModule) return [...new Set([...modules, clientHeadModule])];
3042
+ }
3043
+ };
3044
+ const configuredBasePlugin = {
3045
+ name: "pracht:configured-base",
3046
+ config: {
3047
+ order: "post",
3048
+ handler(config) {
3049
+ configuredBase = typeof config.base === "string" ? config.base : void 0;
3050
+ }
2794
3051
  }
2795
3052
  };
2796
3053
  const clientModuleTransformPlugin = {
@@ -2798,7 +3055,7 @@ function pracht(options = {}) {
2798
3055
  enforce: "post",
2799
3056
  transform(code, id, transformOptions) {
2800
3057
  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.`);
2801
- if (!(isPrachtClientModuleId(id) || !transformOptions?.ssr && isRouteOrShellFile(id, routeFileDirs))) return null;
3058
+ if (!(isPrachtClientModuleId(id) || !transformOptions?.ssr && isRouteOrShellFile(id, routeFileDirs, routeFileExtensions))) return null;
2802
3059
  const transformed = stripServerOnlyExportsForClient(code, id);
2803
3060
  if (transformed === code) return null;
2804
3061
  return {
@@ -2812,7 +3069,7 @@ function pracht(options = {}) {
2812
3069
  name: "pracht:optimize-deps-entries",
2813
3070
  enforce: "post",
2814
3071
  config(config) {
2815
- return withPrachtOptimizeDepsEntries(config, createPrachtOptimizeDepsEntries(resolved), createPrachtOptimizeDepsInclude(config.root ?? process.cwd()));
3072
+ return withPrachtOptimizeDepsEntries(config, resolved, createPrachtOptimizeDepsInclude(config.root ?? process.cwd()));
2816
3073
  }
2817
3074
  };
2818
3075
  const precompilePlugin = resolved.precompileSsrJsx ? preactSsrPrecompile({
@@ -2823,6 +3080,7 @@ function pracht(options = {}) {
2823
3080
  ...precompilePlugin ? [precompilePlugin] : [],
2824
3081
  ...preact(),
2825
3082
  prachtPlugin,
3083
+ configuredBasePlugin,
2826
3084
  clientModuleTransformPlugin,
2827
3085
  ...edgeRuntimeSafetyPlugin ? [edgeRuntimeSafetyPlugin] : [],
2828
3086
  createEnvSafetyPlugin(resolved.envSafety)
@@ -2832,6 +3090,26 @@ function pracht(options = {}) {
2832
3090
  plugins.push(optimizeDepsEntriesPlugin);
2833
3091
  return plugins;
2834
3092
  }
3093
+ function assertSafeRootAbsoluteDeployBase(base) {
3094
+ if (typeof base !== "string" || !base.startsWith("/") || base.startsWith("//")) return;
3095
+ let safe = !base.includes("?") && !base.includes("#");
3096
+ if (safe) try {
3097
+ const segments = base.split("/");
3098
+ safe = segments.every((segment, index) => {
3099
+ if (segment === "" && index !== 0 && index !== segments.length - 1) return false;
3100
+ const decoded = decodeURIComponent(segment);
3101
+ if (decoded === "." || decoded === "..") return false;
3102
+ for (const character of decoded) {
3103
+ const codePoint = character.codePointAt(0);
3104
+ if (character === "/" || character === "\\" || codePoint === 0 || codePoint !== void 0 && (codePoint <= 31 || codePoint === 127)) return false;
3105
+ }
3106
+ return true;
3107
+ });
3108
+ } catch {
3109
+ safe = false;
3110
+ }
3111
+ 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.`);
3112
+ }
2835
3113
  function isGraphOnlyMode() {
2836
3114
  return process.env[PRACHT_GRAPH_ONLY_ENV] === "1";
2837
3115
  }
@@ -2915,6 +3193,7 @@ const PRACHT_OPTIMIZE_DEPS_INCLUDE = [
2915
3193
  "@pracht/core/manifest"
2916
3194
  ];
2917
3195
  const PREACT_DEDUPE = ["preact", "preact-render-to-string"];
3196
+ const PRACHT_SSR_NO_EXTERNAL = /^@pracht\//;
2918
3197
  function createPrachtOptimizeDepsInclude(root) {
2919
3198
  try {
2920
3199
  if (!toPosixPath(createRequire(join(root, "package.json")).resolve("@pracht/core/package.json")).includes("/node_modules/")) return [];
@@ -2923,8 +3202,9 @@ function createPrachtOptimizeDepsInclude(root) {
2923
3202
  return [];
2924
3203
  }
2925
3204
  }
2926
- function withPrachtOptimizeDepsEntries(config, prachtEntries, prachtInclude) {
2927
- const environments = Object.fromEntries(Object.entries(config.environments ?? {}).map(([name, environment]) => [name, { optimizeDeps: { entries: mergeOptimizeDepsEntries(environment.optimizeDeps?.entries, prachtEntries) } }]));
3205
+ function withPrachtOptimizeDepsEntries(config, resolved, prachtInclude) {
3206
+ const prachtEntries = createPrachtOptimizeDepsEntries(resolved, config.optimizeDeps?.extensions);
3207
+ 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)) } }]));
2928
3208
  return {
2929
3209
  optimizeDeps: {
2930
3210
  entries: mergeOptimizeDepsEntries(config.optimizeDeps?.entries, prachtEntries),
@@ -2933,9 +3213,24 @@ function withPrachtOptimizeDepsEntries(config, prachtEntries, prachtInclude) {
2933
3213
  ...Object.keys(environments).length > 0 ? { environments } : {}
2934
3214
  };
2935
3215
  }
2936
- function createPrachtOptimizeDepsEntries(resolved) {
3216
+ const VITE_SCANNABLE_ROUTE_EXTENSIONS = new Set([
3217
+ ".ts",
3218
+ ".tsx",
3219
+ ".js",
3220
+ ".jsx",
3221
+ ".mts",
3222
+ ".mjs",
3223
+ ".cts",
3224
+ ".cjs",
3225
+ ".vue",
3226
+ ".svelte",
3227
+ ".astro",
3228
+ ".imba"
3229
+ ]);
3230
+ function createPrachtOptimizeDepsEntries(resolved, optimizerExtensions) {
2937
3231
  const scriptExtensions = "{ts,tsx,js,jsx}";
2938
- const routeExtensions = "{ts,tsx,js,jsx,md,mdx,tsrx}";
3232
+ const explicitlyScannable = new Set(optimizerExtensions ?? []);
3233
+ const routeExtensions = extensionGlob([...new Set([...DEFAULT_ROUTE_EXTENSIONS, ...resolved.additionalExtensions])].filter((extension) => VITE_SCANNABLE_ROUTE_EXTENSIONS.has(extension) || explicitlyScannable.has(extension)));
2939
3234
  const apiDir = toOptimizeDepsEntry(resolved.apiDir);
2940
3235
  const apiEntries = [`${apiDir}/**/*.{ts,js,tsx,jsx}`, `!${apiDir}/**/*.d.ts`];
2941
3236
  const entries = resolved.pagesDir ? [
@@ -2985,15 +3280,6 @@ function invalidateVirtualModules(server) {
2985
3280
  if (serverMod) server.moduleGraph.invalidateModule(serverMod);
2986
3281
  if (devMod) server.moduleGraph.invalidateModule(devMod);
2987
3282
  }
2988
- const ROUTE_FILE_EXTENSIONS = new Set([
2989
- ".ts",
2990
- ".tsx",
2991
- ".js",
2992
- ".jsx",
2993
- ".md",
2994
- ".mdx",
2995
- ".tsrx"
2996
- ]);
2997
3283
  function computeRouteFileDirs(root, resolved) {
2998
3284
  return (resolved.pagesDir ? [resolved.pagesDir] : [resolved.routesDir, resolved.shellsDir]).map((dir) => canonicalFilePath(resolveConfigPath(root, dir))).map(withTrailingSep);
2999
3285
  }
@@ -3024,7 +3310,7 @@ function canonicalFilePath(path) {
3024
3310
  return toPosixPath(path);
3025
3311
  }
3026
3312
  }
3027
- function isRouteOrShellFile(id, dirs) {
3313
+ function isRouteOrShellFile(id, dirs, extensions) {
3028
3314
  if (dirs.length === 0) return false;
3029
3315
  const queryStart = id.indexOf("?");
3030
3316
  const path = queryStart === -1 ? id : id.slice(0, queryStart);
@@ -3032,7 +3318,7 @@ function isRouteOrShellFile(id, dirs) {
3032
3318
  const extIndex = path.lastIndexOf(".");
3033
3319
  if (extIndex === -1) return false;
3034
3320
  const ext = path.slice(extIndex);
3035
- if (!ROUTE_FILE_EXTENSIONS.has(ext)) return false;
3321
+ if (!extensions.has(ext)) return false;
3036
3322
  const normalized = toPosixPath(path);
3037
3323
  return dirs.some((dir) => normalized.startsWith(dir));
3038
3324
  }