@pracht/vite-plugin 0.8.0 → 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,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();
@@ -1350,6 +1358,7 @@ const DEFAULTS = {
1350
1358
  shellsDir: "/src/shells",
1351
1359
  apiDir: "/src/api",
1352
1360
  serverDir: "/src/server",
1361
+ additionalExtensions: [],
1353
1362
  islandsDir: "/src/islands",
1354
1363
  capabilitiesDir: "/src/capabilities",
1355
1364
  adapter: createDefaultNodeAdapter(),
@@ -1368,6 +1377,7 @@ function resolveOptions(options) {
1368
1377
  ...options
1369
1378
  };
1370
1379
  if (resolved.llmsTxt === void 0) resolved.llmsTxt = false;
1380
+ resolved.additionalExtensions = normalizeAdditionalExtensions(resolved.additionalExtensions);
1371
1381
  if (!new Set([
1372
1382
  "spa",
1373
1383
  "ssr",
@@ -1582,7 +1592,7 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
1582
1592
  "// Generated by @pracht/vite-plugin from the app manifest capability registrations.",
1583
1593
  "// Contains only http-exposed capability names, endpoints, and effects —",
1584
1594
  "// capability modules themselves are server-only and never reach the client graph.",
1585
- "import { createUseCapability } from \"@pracht/core\";",
1595
+ "import { createUseCapability, withBase } from \"@pracht/core\";",
1586
1596
  "",
1587
1597
  `const endpoints = Object.assign(Object.create(null), JSON.parse(${JSON.stringify(JSON.stringify(endpoints))}));`,
1588
1598
  "",
@@ -1598,7 +1608,7 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
1598
1608
  " } else if (opts && opts.confirm) {",
1599
1609
  ` headers.set(${JSON.stringify(CONFIRMATION_HEADER)}, opts.confirm);`,
1600
1610
  " }",
1601
- " response = await fetch(endpoint.path, {",
1611
+ " response = await fetch(withBase(endpoint.path), {",
1602
1612
  " method: endpoint.method,",
1603
1613
  " headers,",
1604
1614
  " body: JSON.stringify(input === undefined ? {} : input),",
@@ -1791,15 +1801,6 @@ function hasWebmcpCapabilities(options = {}, root = process.cwd()) {
1791
1801
  }
1792
1802
  //#endregion
1793
1803
  //#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
1804
  const NON_FULL_HYDRATION_RE = /hydration\s*:\s*["'](?:islands|none)["']/;
1804
1805
  const FULL_HYDRATION_RE = /hydration\s*:\s*["']full["']/;
1805
1806
  const PAGES_NON_FULL_HYDRATION_RE = /export\s+const\s+HYDRATION\s*=\s*["'](?:islands|none)["']/;
@@ -1818,7 +1819,7 @@ function findMatching(source, start, open, close) {
1818
1819
  }
1819
1820
  return -1;
1820
1821
  }
1821
- function scanFiles(dir, files) {
1822
+ function scanFiles(dir, files, extensions) {
1822
1823
  let entries;
1823
1824
  try {
1824
1825
  entries = readdirSync(dir);
@@ -1833,15 +1834,15 @@ function scanFiles(dir, files) {
1833
1834
  } catch {
1834
1835
  continue;
1835
1836
  }
1836
- if (stat.isDirectory()) scanFiles(abs, files);
1837
- 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);
1838
1839
  }
1839
1840
  }
1840
1841
  function createNonFullHydrationExcludes(resolved, root = process.cwd()) {
1841
1842
  const excludes = /* @__PURE__ */ new Set();
1842
1843
  if (resolved.pagesDir) {
1843
1844
  const files = [];
1844
- scanFiles(resolve(root, resolved.pagesDir.replace(/^\//, "")), files);
1845
+ scanFiles(resolve(root, resolved.pagesDir.replace(/^\//, "")), files, withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, resolved.additionalExtensions));
1845
1846
  for (const file of files) try {
1846
1847
  if (PAGES_NON_FULL_HYDRATION_RE.test(readFileSync(file, "utf-8"))) excludes.add(`!/${toPosixPath$1(file).replace(toPosixPath$1(root).replace(/\/$/, "") + "/", "")}`);
1847
1848
  } catch {}
@@ -1891,15 +1892,18 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
1891
1892
  const resolved = resolveOptions(options);
1892
1893
  const isPagesMode = !!resolved.pagesDir;
1893
1894
  const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
1895
+ const routeHeadHints = createRouteHeadHintsForVirtualModules(resolved, buildOptions.root);
1896
+ const routeStaticPathsHints = createRouteStaticPathsHintsForVirtualModules(resolved, buildOptions.root);
1894
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)];
1895
1899
  const dirPrefix = isPagesMode ? resolved.pagesDir : resolved.routesDir;
1896
1900
  const routeGlob = `${dirPrefix}/**/*.{ts,tsx,js,jsx,md,mdx}`;
1897
- const routeTsrxGlob = `${dirPrefix}/**/*.tsrx`;
1901
+ const additionalRouteGlob = `${dirPrefix}/**/*.${extensionGlob(bareRouteExtensions)}`;
1898
1902
  const routeExcludes = createNonFullHydrationExcludes(resolved, buildOptions.root);
1899
1903
  const routeGlobPattern = routeExcludes.length > 0 ? [routeGlob, ...routeExcludes] : routeGlob;
1900
- const routeTsrxGlobPattern = routeExcludes.length > 0 ? [routeTsrxGlob, ...routeExcludes] : routeTsrxGlob;
1904
+ const additionalRouteGlobPattern = additionalRouteGlob && routeExcludes.length > 0 ? [additionalRouteGlob, ...routeExcludes] : additionalRouteGlob;
1901
1905
  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`;
1906
+ const additionalShellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.${extensionGlob(bareRouteExtensions)}` : `${resolved.shellsDir}/**/*.${extensionGlob(bareRouteExtensions)}`;
1903
1907
  const appFilePosix = resolved.appFile.replace(/\\/g, "/").replace(/^\.\//, "");
1904
1908
  const appDir = (appFilePosix.startsWith("/") ? appFilePosix : `/${appFilePosix}`).replace(/\/[^/]*$/, "") || "/";
1905
1909
  return [
@@ -1907,17 +1911,19 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
1907
1911
  appImport,
1908
1912
  "",
1909
1913
  `const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
1914
+ `const routeHeadHints = ${JSON.stringify(routeHeadHints)};`,
1915
+ `const routeStaticPathsHints = ${JSON.stringify(routeStaticPathsHints)};`,
1910
1916
  `const routeModules = {`,
1911
1917
  ` ...import.meta.glob(${JSON.stringify(routeGlobPattern)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`,
1912
- ` ...import.meta.glob(${JSON.stringify(routeTsrxGlobPattern)}),`,
1918
+ ` ...import.meta.glob(${JSON.stringify(additionalRouteGlobPattern)}),`,
1913
1919
  `};`,
1914
1920
  `const shellModules = {`,
1915
1921
  ` ...import.meta.glob(${JSON.stringify(shellGlob)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`,
1916
- ` ...import.meta.glob(${JSON.stringify(shellTsrxGlob)}),`,
1922
+ ` ...import.meta.glob(${JSON.stringify(additionalShellGlob)}),`,
1917
1923
  `};`,
1918
1924
  "",
1919
1925
  "const resolvedApp = resolveApp(app);",
1920
- "applyRouteLoaderHints(resolvedApp, routeLoaderHints);",
1926
+ "applyRouteHints(resolvedApp, routeLoaderHints, routeHeadHints, routeStaticPathsHints);",
1921
1927
  "",
1922
1928
  ...createApplyRouteLoaderHintsSource(),
1923
1929
  `const APP_DIR = ${JSON.stringify(appDir)};`,
@@ -2013,7 +2019,9 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
2013
2019
  const isPagesMode = !!resolved.pagesDir;
2014
2020
  const registrySource = createPrachtRegistryModuleSource(resolved);
2015
2021
  const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
2016
- 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 ?? "/") : {
2017
2025
  clientEntryUrl: null,
2018
2026
  islandsEntryUrl: null,
2019
2027
  cssManifest: {},
@@ -2025,7 +2033,10 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
2025
2033
  let prachtImports = adapter?.serverImports ? adapter.serverImports + "\nimport { prerenderApp } from \"@pracht/core/server\";" : "import { resolveApp, resolveApiRoutes, prerenderApp } from \"@pracht/core/server\";";
2026
2034
  if (llmsTxtConfig) prachtImports += "\nimport { buildLlmsTxt } from \"@pracht/core/server\";";
2027
2035
  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;
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);
2029
2040
  const islandsGlob = `${resolved.islandsDir}/**/*.{ts,tsx,js,jsx}`;
2030
2041
  const source = [
2031
2042
  prachtImports,
@@ -2033,6 +2044,8 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
2033
2044
  appImport,
2034
2045
  "",
2035
2046
  `const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
2047
+ `const routeHeadHints = ${JSON.stringify(routeHeadHints)};`,
2048
+ `const routeStaticPathsHints = ${JSON.stringify(routeStaticPathsHints)};`,
2036
2049
  ...createApplyRouteLoaderHintsSource(),
2037
2050
  registrySource,
2038
2051
  "",
@@ -2044,10 +2057,13 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
2044
2057
  "export const islandFiles = Object.keys(islandModules);",
2045
2058
  "",
2046
2059
  "export const resolvedApp = resolveApp(app);",
2047
- "applyRouteLoaderHints(resolvedApp, routeLoaderHints);",
2060
+ "applyRouteHints(resolvedApp, routeLoaderHints, routeHeadHints, routeStaticPathsHints);",
2048
2061
  `export const apiRoutes = resolveApiRoutes(Object.keys(apiModules), ${JSON.stringify(resolved.apiDir)});`,
2049
2062
  `export const buildTarget = ${JSON.stringify(adapter?.id ?? "node")};`,
2050
- `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")};`,
2051
2067
  `export const islandsEntryUrl = ${JSON.stringify(islandsEntryUrl ?? null)};`,
2052
2068
  `export const islandsBootstrapRequired = ${JSON.stringify(islandsBootstrapRequired)};`,
2053
2069
  `export const cssManifest = ${JSON.stringify(clientBuild.cssManifest)};`,
@@ -2075,15 +2091,25 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
2075
2091
  */
2076
2092
  function createPrachtDevModuleSource(options = {}, buildOptions = {}) {
2077
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);
2078
2097
  return [
2079
2098
  "import { resolveApp, resolveApiRoutes } from \"@pracht/core/server\";",
2080
2099
  resolved.pagesDir ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`,
2081
2100
  "",
2101
+ `const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
2102
+ `const routeHeadHints = ${JSON.stringify(routeHeadHints)};`,
2103
+ `const routeStaticPathsHints = ${JSON.stringify(routeStaticPathsHints)};`,
2104
+ ...createApplyRouteLoaderHintsSource(),
2082
2105
  createPrachtRegistryModuleSource(resolved),
2083
2106
  "",
2084
2107
  "export const resolvedApp = resolveApp(app);",
2108
+ "applyRouteHints(resolvedApp, routeLoaderHints, routeHeadHints, routeStaticPathsHints);",
2085
2109
  `export const apiRoutes = resolveApiRoutes(Object.keys(apiModules), ${JSON.stringify(resolved.apiDir)});`,
2086
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 ?? "/")};`,
2087
2113
  ""
2088
2114
  ].join("\n");
2089
2115
  }
@@ -2108,7 +2134,7 @@ function resolveLlmsTxtConfig(resolved, root = process.cwd()) {
2108
2134
  }
2109
2135
  function createApplyRouteLoaderHintsSource() {
2110
2136
  return [
2111
- "function applyRouteLoaderHints(resolvedApp, routeLoaderHints) {",
2137
+ "function applyRouteHints(resolvedApp, routeLoaderHints, routeHeadHints, routeStaticPathsHints) {",
2112
2138
  " for (const route of resolvedApp.routes) {",
2113
2139
  " const hint = routeLoaderHints[route.file];",
2114
2140
  " if (hint === true) {",
@@ -2116,14 +2142,52 @@ function createApplyRouteLoaderHintsSource() {
2116
2142
  " } else if (typeof route.hasLoader === 'undefined' && typeof hint === 'boolean') {",
2117
2143
  " route.hasLoader = hint;",
2118
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
+ " }",
2119
2160
  " }",
2120
2161
  "}",
2121
2162
  ""
2122
2163
  ];
2123
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
+ }
2124
2188
  function createRouteLoaderHintsForVirtualModules(options, root = process.cwd()) {
2125
2189
  if (options.pagesDir) {
2126
- const pages = scanPagesDirectory(resolve(root, options.pagesDir.slice(1)));
2190
+ const pages = scanPagesDirectory(resolve(root, options.pagesDir.slice(1)), options.additionalExtensions);
2127
2191
  const hints = {};
2128
2192
  for (const page of pages) {
2129
2193
  const key = `${options.pagesDir}/${page.relativePath.replace(/\\/g, "/")}`;
@@ -2133,6 +2197,7 @@ function createRouteLoaderHintsForVirtualModules(options, root = process.cwd())
2133
2197
  }
2134
2198
  const appFileDir = dirname(resolve(root, options.appFile.slice(1)));
2135
2199
  return createRouteLoaderHints(resolve(root, options.routesDir.slice(1)), {
2200
+ additionalExtensions: options.additionalExtensions,
2136
2201
  appFileDir,
2137
2202
  rootRelativePrefix: options.routesDir
2138
2203
  });
@@ -2141,18 +2206,19 @@ function createPrachtRegistryModuleSource(options = {}) {
2141
2206
  const resolved = resolveOptions(options);
2142
2207
  const apiGlobs = [`${resolved.apiDir}/**/*.{ts,js,tsx,jsx}`, `!${resolved.apiDir}/**/*.d.ts`];
2143
2208
  const isPagesMode = !!resolved.pagesDir;
2209
+ const bareRouteExtensions = [...withAdditionalExtensions(LEGACY_BARE_ROUTE_EXTENSIONS, resolved.additionalExtensions)];
2144
2210
  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`;
2211
+ const additionalRouteGlob = `${isPagesMode ? resolved.pagesDir : resolved.routesDir}/**/*.${extensionGlob(bareRouteExtensions)}`;
2146
2212
  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`;
2213
+ const additionalShellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.${extensionGlob(bareRouteExtensions)}` : `${resolved.shellsDir}/**/*.${extensionGlob(bareRouteExtensions)}`;
2148
2214
  return [
2149
2215
  `export const routeModules = {`,
2150
2216
  ` ...import.meta.glob(${JSON.stringify(routeGlob)}),`,
2151
- ` ...import.meta.glob(${JSON.stringify(routeTsrxGlob)}),`,
2217
+ ` ...import.meta.glob(${JSON.stringify(additionalRouteGlob)}),`,
2152
2218
  `};`,
2153
2219
  `export const shellModules = {`,
2154
2220
  ` ...import.meta.glob(${JSON.stringify(shellGlob)}),`,
2155
- ` ...import.meta.glob(${JSON.stringify(shellTsrxGlob)}),`,
2221
+ ` ...import.meta.glob(${JSON.stringify(additionalShellGlob)}),`,
2156
2222
  `};`,
2157
2223
  `export const middlewareModules = import.meta.glob(${JSON.stringify(`${resolved.middlewareDir}/**/*.{ts,tsx,js,jsx}`)});`,
2158
2224
  `export const apiModules = import.meta.glob(${JSON.stringify(apiGlobs)});`,
@@ -2176,13 +2242,15 @@ function clearPagesAppSourceCache() {
2176
2242
  function generatePagesAppInlineSource(options, root = process.cwd()) {
2177
2243
  const absPagesDir = resolve(root, options.pagesDir.slice(1));
2178
2244
  const cacheKey = JSON.stringify({
2245
+ additionalExtensions: options.additionalExtensions,
2179
2246
  absPagesDir,
2180
2247
  pagesDefaultRender: options.pagesDefaultRender,
2181
2248
  pagesDirPrefix: options.pagesDir
2182
2249
  });
2183
2250
  const cached = pagesAppSourceCache.get(cacheKey);
2184
2251
  if (cached) return cached;
2185
- const source = generatePagesManifestSource(scanPagesDirectory(absPagesDir), {
2252
+ const source = generatePagesManifestSource(scanPagesDirectory(absPagesDir, options.additionalExtensions), {
2253
+ additionalExtensions: options.additionalExtensions,
2186
2254
  pagesDir: absPagesDir,
2187
2255
  pagesDefaultRender: options.pagesDefaultRender,
2188
2256
  pagesDirPrefix: options.pagesDir
@@ -2197,8 +2265,46 @@ const DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
2197
2265
  const CSS_MODULE_URL_RE = /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
2198
2266
  const DEVTOOLS_JSON_PATH = "/_pracht.json";
2199
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
+ }
2200
2304
  function createDevSSRMiddleware(server, options = {}) {
2201
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)}`;
2202
2308
  let warnedDevtoolsCollision = false;
2203
2309
  let warnedLlmsTxtCollision = false;
2204
2310
  if (options.llmsTxt && typeof server.config.publicDir === "string") {
@@ -2223,6 +2329,7 @@ function createDevSSRMiddleware(server, options = {}) {
2223
2329
  await serveDevtools(server, res, {
2224
2330
  apiRoutes: serverMod.apiRoutes ?? [],
2225
2331
  app: serverMod.resolvedApp,
2332
+ base: devBase,
2226
2333
  url,
2227
2334
  wantsJson: requestUrl.pathname === DEVTOOLS_JSON_PATH
2228
2335
  });
@@ -2243,10 +2350,10 @@ function createDevSSRMiddleware(server, options = {}) {
2243
2350
  return;
2244
2351
  }
2245
2352
  if (shouldBypassDevSSR(requestUrl, req, routeMatchers)) return next();
2246
- 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);
2247
2354
  let webRequest;
2248
2355
  try {
2249
- webRequest = await nodeToWebRequest(req, maxBodySize);
2356
+ webRequest = await nodeToWebRequest(req, maxBodySize, devBase);
2250
2357
  } catch (err) {
2251
2358
  if (err instanceof Error && err.message === "Request body too large") {
2252
2359
  res.statusCode = 413;
@@ -2261,8 +2368,8 @@ function createDevSSRMiddleware(server, options = {}) {
2261
2368
  registry: serverMod.registry,
2262
2369
  request: webRequest,
2263
2370
  debugErrors: true,
2264
- clientEntryUrl: CLIENT_BROWSER_PATH,
2265
- islandsEntryUrl: ISLANDS_CLIENT_BROWSER_PATH,
2371
+ clientEntryUrl: withDevBase(CLIENT_BROWSER_PATH),
2372
+ islandsEntryUrl: withDevBase(ISLANDS_CLIENT_BROWSER_PATH),
2266
2373
  islandsBootstrapRequired: serverMod.islandsBootstrapRequired === true,
2267
2374
  apiRoutes: serverMod.apiRoutes,
2268
2375
  timings
@@ -2270,8 +2377,27 @@ function createDevSSRMiddleware(server, options = {}) {
2270
2377
  const responseContentType = response.headers.get("content-type") ?? "";
2271
2378
  if (response.status === 404 && !responseContentType.includes("application/json") && !routeMatchers.app?.notFound) return next();
2272
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
+ }
2273
2399
  let body = await response.text();
2274
- if (contentType.includes("text/html")) body = await server.transformIndexHtml(url, body);
2400
+ if (contentType.includes("text/html")) body = await transformDevHtml(server, url, body, devBase);
2275
2401
  res.statusCode = response.status;
2276
2402
  response.headers.forEach((value, key) => {
2277
2403
  res.setHeader(key, value);
@@ -2280,7 +2406,56 @@ function createDevSSRMiddleware(server, options = {}) {
2280
2406
  if (serverTiming) res.setHeader("Server-Timing", serverTiming);
2281
2407
  res.end(body);
2282
2408
  } catch (error) {
2283
- 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;
2284
2459
  }
2285
2460
  };
2286
2461
  }
@@ -2292,7 +2467,7 @@ function createDevSSRMiddleware(server, options = {}) {
2292
2467
  * avoid a first-paint FOUC.
2293
2468
  */
2294
2469
  async function createDevCssManifest(server, options) {
2295
- 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;
2296
2471
  if (!route) return {};
2297
2472
  const manifest = {};
2298
2473
  const modules = [...route.shellFile ? [{
@@ -2341,18 +2516,19 @@ function collectDevCssUrls(entry) {
2341
2516
  }
2342
2517
  return [...urls];
2343
2518
  }
2344
- function injectDevCssLinks(html, manifest) {
2519
+ function injectDevCssLinks(html, manifest, base = "/") {
2345
2520
  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}">`);
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}">`);
2347
2522
  if (tags.length === 0) return html;
2348
2523
  return html.replace("</head>", ` ${tags.join("\n ")}\n </head>`);
2349
2524
  }
2350
- async function injectDevCssForPath(server, path, html) {
2351
- 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 || "/");
2352
2527
  }
2353
- async function resolveDevCssContextForPath(server, path) {
2528
+ async function resolveDevCssContextForPath(server, path, options = {}) {
2354
2529
  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;
2530
+ const publicPathname = new URL(path, "http://localhost").pathname;
2531
+ const pathname = options.basePathRetained ? framework.stripBase(publicPathname) : publicPathname;
2356
2532
  return {
2357
2533
  app: serverMod.resolvedApp,
2358
2534
  matchAppRoute: framework.matchAppRoute,
@@ -2374,7 +2550,7 @@ function createDevCssInjectionMiddleware(server) {
2374
2550
  next();
2375
2551
  return;
2376
2552
  }
2377
- const contextPromise = resolveDevCssContextForPath(server, req.url ?? "/").catch((error) => {
2553
+ const contextPromise = resolveDevCssContextForPath(server, req.url ?? "/", { basePathRetained: true }).catch((error) => {
2378
2554
  if (!warned) {
2379
2555
  warned = true;
2380
2556
  server.config.logger.warn(`[pracht] Could not discover development stylesheets: ${error instanceof Error ? error.message : String(error)}`);
@@ -2405,7 +2581,7 @@ function createDevCssInjectionMiddleware(server) {
2405
2581
  try {
2406
2582
  const context = await contextPromise;
2407
2583
  const manifest = context ? await createDevCssManifest(server, context) : null;
2408
- 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);
2409
2585
  } catch {
2410
2586
  originalEnd(body, done);
2411
2587
  }
@@ -2453,13 +2629,13 @@ async function serveDevtools(server, res, options) {
2453
2629
  res.end(JSON.stringify(graph, null, 2));
2454
2630
  return;
2455
2631
  }
2456
- let html = devtools.buildDevtoolsHtml(graph);
2632
+ let html = devtools.buildDevtoolsHtml(graph, { base: options.base });
2457
2633
  html = await server.transformIndexHtml(options.url, html);
2458
2634
  res.statusCode = 200;
2459
2635
  res.setHeader("content-type", "text/html; charset=utf-8");
2460
2636
  res.end(html);
2461
2637
  }
2462
- async function handleDevError(server, req, res, next, url, error) {
2638
+ async function handleDevError(server, req, res, next, url, error, base) {
2463
2639
  if (error instanceof Error) server.ssrFixStacktrace(error);
2464
2640
  if (req.headers["x-pracht-route-state-request"] === "1") {
2465
2641
  res.statusCode = 500;
@@ -2476,7 +2652,8 @@ async function handleDevError(server, req, res, next, url, error) {
2476
2652
  let html = buildErrorOverlayHtml({
2477
2653
  message: error instanceof Error ? error.message : String(error),
2478
2654
  stack: error instanceof Error ? error.stack : void 0,
2479
- root: server.config.root
2655
+ root: server.config.root,
2656
+ base
2480
2657
  });
2481
2658
  html = await server.transformIndexHtml(url, html);
2482
2659
  res.statusCode = 500;
@@ -2505,11 +2682,12 @@ function isDevNotFoundRequest(requestUrl, req, options = {}) {
2505
2682
  if (!accept.includes("text/html") && !accept.includes("application/xhtml+xml")) return false;
2506
2683
  return !matchesResolvedRoute(url.pathname, options);
2507
2684
  }
2508
- async function serveDevNotFound(server, res, next, url, pathname, options) {
2685
+ async function serveDevNotFound(server, res, next, url, pathname, options, base) {
2509
2686
  try {
2510
2687
  const { buildDevNotFoundHtml } = await server.ssrLoadModule("@pracht/core/dev-404");
2511
2688
  let html = buildDevNotFoundHtml({
2512
2689
  apiRoutes: options.apiRoutes.map((route) => ({ path: route.path })),
2690
+ base,
2513
2691
  requestedPath: pathname,
2514
2692
  routes: options.app.routes.map((route) => ({
2515
2693
  path: route.path,
@@ -2604,10 +2782,11 @@ const DEV_ASSET_EXTENSIONS = new Set([
2604
2782
  ".woff2",
2605
2783
  ".xml"
2606
2784
  ]);
2607
- async function nodeToWebRequest(req, maxBodySize) {
2785
+ async function nodeToWebRequest(req, maxBodySize, base = "/") {
2608
2786
  const protocol = "http";
2609
2787
  const host = req.headers.host ?? "localhost";
2610
- 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}`);
2611
2790
  const method = req.method ?? "GET";
2612
2791
  const headers = new Headers();
2613
2792
  for (const [key, value] of Object.entries(req.headers)) {
@@ -2635,14 +2814,34 @@ async function nodeToWebRequest(req, maxBodySize) {
2635
2814
  }
2636
2815
  //#endregion
2637
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
+ }
2638
2833
  function pracht(options = {}) {
2639
2834
  const resolved = resolveOptions(options);
2640
2835
  const isPagesMode = !!resolved.pagesDir;
2641
2836
  let root = process.cwd();
2642
2837
  let routeFileDirs = [];
2838
+ let clientRouteHeadHints = {};
2839
+ const routeFileExtensions = withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, resolved.additionalExtensions);
2643
2840
  let capabilityModulePaths = /* @__PURE__ */ new Set();
2644
2841
  if (isPagesMode && options.appFile) console.warn("[pracht] Both `pagesDir` and `appFile` are set. `pagesDir` takes precedence — `appFile` will be ignored.");
2645
2842
  let isBuild = false;
2843
+ let base = "/";
2844
+ let configuredBase;
2646
2845
  const prachtPlugin = {
2647
2846
  name: "pracht",
2648
2847
  enforce: "pre",
@@ -2654,13 +2853,15 @@ function pracht(options = {}) {
2654
2853
  const envDir = _config.envDir ? resolve(configRoot, _config.envDir) : configRoot;
2655
2854
  const publicEnvDefine = JSON.stringify(loadEnv(env.mode, envDir, PUBLIC_ENV_PREFIX));
2656
2855
  const agentSurfaceDefine = env.command === "build" ? String(hasAgentSurface(resolved, configRoot)) : "true";
2856
+ const staticTargetDefine = String(env.command === "build" && resolved.adapter.staticTarget === true);
2657
2857
  return {
2658
2858
  appType: "custom",
2659
2859
  envPrefix: ["VITE_", PUBLIC_ENV_PREFIX],
2660
2860
  resolve: { dedupe: PREACT_DEDUPE },
2661
2861
  define: {
2662
2862
  __PRACHT_PUBLIC_ENV__: publicEnvDefine,
2663
- __PRACHT_AGENT_SURFACE__: agentSurfaceDefine
2863
+ __PRACHT_AGENT_SURFACE__: agentSurfaceDefine,
2864
+ __PRACHT_STATIC_TARGET__: staticTargetDefine
2664
2865
  },
2665
2866
  ...isSSRBuild ? {} : { build: { rollupOptions: {
2666
2867
  ...wantsIslandsEntry ? { input: [PRACHT_ISLANDS_CLIENT_MODULE_ID] } : {},
@@ -2683,12 +2884,15 @@ function pracht(options = {}) {
2683
2884
  external: ["node:module"]
2684
2885
  } } },
2685
2886
  build: { rollupOptions: { external: [/^cloudflare:/] } }
2686
- } : {}
2887
+ } : {},
2888
+ ...!isEdge && isSSRBuild ? { ssr: { noExternal: [PRACHT_SSR_NO_EXTERNAL] } } : {}
2687
2889
  };
2688
2890
  },
2689
2891
  configResolved(config) {
2892
+ assertSafeRootAbsoluteDeployBase(config.base);
2690
2893
  root = config.root;
2691
2894
  isBuild = config.command === "build";
2895
+ base = config.base;
2692
2896
  routeFileDirs = computeRouteFileDirs(root, resolved);
2693
2897
  capabilityModulePaths = new Set(resolveCapabilityModulePaths(resolved, root).map(canonicalFilePath));
2694
2898
  },
@@ -2704,11 +2908,19 @@ function pracht(options = {}) {
2704
2908
  },
2705
2909
  load(id) {
2706
2910
  if (isIslandsClientModule(id)) return createPrachtIslandsClientModuleSource(resolved, { root });
2707
- if (isClientModule(id)) return createPrachtClientModuleSource(resolved, { root });
2708
- 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
+ });
2709
2919
  if (isServerModule(id)) return createPrachtServerModuleSource(resolved, {
2710
2920
  root,
2711
- isBuild
2921
+ isBuild,
2922
+ base,
2923
+ configuredBase
2712
2924
  });
2713
2925
  if (isCapabilitiesModule(id)) return createPrachtCapabilitiesClientModuleSource(resolved, { root });
2714
2926
  if (isWebmcpModule(id)) return createPrachtWebmcpModuleSource(resolved, { root });
@@ -2727,6 +2939,7 @@ function pracht(options = {}) {
2727
2939
  configureServer(server) {
2728
2940
  if (isPagesMode) watchPagesDirectory(server, resolved, root);
2729
2941
  if (resolved.adapter.ownsDevServer) {
2942
+ server.middlewares.use(createOwnedDevEntryMiddleware(server));
2730
2943
  server.middlewares.use(createDevCssInjectionMiddleware(server));
2731
2944
  return;
2732
2945
  }
@@ -2745,14 +2958,29 @@ function pracht(options = {}) {
2745
2958
  return html;
2746
2959
  }
2747
2960
  },
2748
- handleHotUpdate({ file, server }) {
2961
+ handleHotUpdate({ file, modules = [], server }) {
2749
2962
  const serverRoot = toPosixPath(server.config.root);
2750
2963
  const normalizedFile = toPosixPath(file);
2751
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);
2752
2980
  if (isPagesMode && relative.startsWith(resolved.pagesDir)) {
2753
2981
  clearPagesAppSourceCache();
2754
2982
  invalidateVirtualModules(server);
2755
- sendServerOnlyFullReload(server, file);
2983
+ if (!sendServerOnlyFullReload(server, file) && shouldReloadClientHead && clientHeadModule) return [...new Set([...modules, clientHeadModule])];
2756
2984
  return;
2757
2985
  }
2758
2986
  if (!isPagesMode && relative === resolved.appFile) {
@@ -2772,7 +3000,7 @@ function pracht(options = {}) {
2772
3000
  if (serverMod) server.moduleGraph.invalidateModule(serverMod);
2773
3001
  const devMod = server.moduleGraph.getModuleById(PRACHT_DEV_MODULE_ID);
2774
3002
  if (devMod) server.moduleGraph.invalidateModule(devMod);
2775
- if (relative.startsWith(resolved.routesDir)) {
3003
+ if (relative.startsWith(resolved.routesDir) || relative.startsWith(resolved.shellsDir)) {
2776
3004
  const clientMod = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
2777
3005
  if (clientMod) server.moduleGraph.invalidateModule(clientMod);
2778
3006
  }
@@ -2790,7 +3018,16 @@ function pracht(options = {}) {
2790
3018
  if (capabilityMod) server.moduleGraph.invalidateModule(capabilityMod);
2791
3019
  }
2792
3020
  }
2793
- 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
+ }
2794
3031
  }
2795
3032
  };
2796
3033
  const clientModuleTransformPlugin = {
@@ -2798,7 +3035,7 @@ function pracht(options = {}) {
2798
3035
  enforce: "post",
2799
3036
  transform(code, id, transformOptions) {
2800
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.`);
2801
- if (!(isPrachtClientModuleId(id) || !transformOptions?.ssr && isRouteOrShellFile(id, routeFileDirs))) return null;
3038
+ if (!(isPrachtClientModuleId(id) || !transformOptions?.ssr && isRouteOrShellFile(id, routeFileDirs, routeFileExtensions))) return null;
2802
3039
  const transformed = stripServerOnlyExportsForClient(code, id);
2803
3040
  if (transformed === code) return null;
2804
3041
  return {
@@ -2812,7 +3049,7 @@ function pracht(options = {}) {
2812
3049
  name: "pracht:optimize-deps-entries",
2813
3050
  enforce: "post",
2814
3051
  config(config) {
2815
- return withPrachtOptimizeDepsEntries(config, createPrachtOptimizeDepsEntries(resolved), createPrachtOptimizeDepsInclude(config.root ?? process.cwd()));
3052
+ return withPrachtOptimizeDepsEntries(config, resolved, createPrachtOptimizeDepsInclude(config.root ?? process.cwd()));
2816
3053
  }
2817
3054
  };
2818
3055
  const precompilePlugin = resolved.precompileSsrJsx ? preactSsrPrecompile({
@@ -2823,6 +3060,7 @@ function pracht(options = {}) {
2823
3060
  ...precompilePlugin ? [precompilePlugin] : [],
2824
3061
  ...preact(),
2825
3062
  prachtPlugin,
3063
+ configuredBasePlugin,
2826
3064
  clientModuleTransformPlugin,
2827
3065
  ...edgeRuntimeSafetyPlugin ? [edgeRuntimeSafetyPlugin] : [],
2828
3066
  createEnvSafetyPlugin(resolved.envSafety)
@@ -2832,6 +3070,26 @@ function pracht(options = {}) {
2832
3070
  plugins.push(optimizeDepsEntriesPlugin);
2833
3071
  return plugins;
2834
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
+ }
2835
3093
  function isGraphOnlyMode() {
2836
3094
  return process.env[PRACHT_GRAPH_ONLY_ENV] === "1";
2837
3095
  }
@@ -2915,6 +3173,7 @@ const PRACHT_OPTIMIZE_DEPS_INCLUDE = [
2915
3173
  "@pracht/core/manifest"
2916
3174
  ];
2917
3175
  const PREACT_DEDUPE = ["preact", "preact-render-to-string"];
3176
+ const PRACHT_SSR_NO_EXTERNAL = /^@pracht\//;
2918
3177
  function createPrachtOptimizeDepsInclude(root) {
2919
3178
  try {
2920
3179
  if (!toPosixPath(createRequire(join(root, "package.json")).resolve("@pracht/core/package.json")).includes("/node_modules/")) return [];
@@ -2923,8 +3182,9 @@ function createPrachtOptimizeDepsInclude(root) {
2923
3182
  return [];
2924
3183
  }
2925
3184
  }
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) } }]));
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)) } }]));
2928
3188
  return {
2929
3189
  optimizeDeps: {
2930
3190
  entries: mergeOptimizeDepsEntries(config.optimizeDeps?.entries, prachtEntries),
@@ -2933,9 +3193,24 @@ function withPrachtOptimizeDepsEntries(config, prachtEntries, prachtInclude) {
2933
3193
  ...Object.keys(environments).length > 0 ? { environments } : {}
2934
3194
  };
2935
3195
  }
2936
- 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) {
2937
3211
  const scriptExtensions = "{ts,tsx,js,jsx}";
2938
- 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)));
2939
3214
  const apiDir = toOptimizeDepsEntry(resolved.apiDir);
2940
3215
  const apiEntries = [`${apiDir}/**/*.{ts,js,tsx,jsx}`, `!${apiDir}/**/*.d.ts`];
2941
3216
  const entries = resolved.pagesDir ? [
@@ -2985,15 +3260,6 @@ function invalidateVirtualModules(server) {
2985
3260
  if (serverMod) server.moduleGraph.invalidateModule(serverMod);
2986
3261
  if (devMod) server.moduleGraph.invalidateModule(devMod);
2987
3262
  }
2988
- const ROUTE_FILE_EXTENSIONS = new Set([
2989
- ".ts",
2990
- ".tsx",
2991
- ".js",
2992
- ".jsx",
2993
- ".md",
2994
- ".mdx",
2995
- ".tsrx"
2996
- ]);
2997
3263
  function computeRouteFileDirs(root, resolved) {
2998
3264
  return (resolved.pagesDir ? [resolved.pagesDir] : [resolved.routesDir, resolved.shellsDir]).map((dir) => canonicalFilePath(resolveConfigPath(root, dir))).map(withTrailingSep);
2999
3265
  }
@@ -3024,7 +3290,7 @@ function canonicalFilePath(path) {
3024
3290
  return toPosixPath(path);
3025
3291
  }
3026
3292
  }
3027
- function isRouteOrShellFile(id, dirs) {
3293
+ function isRouteOrShellFile(id, dirs, extensions) {
3028
3294
  if (dirs.length === 0) return false;
3029
3295
  const queryStart = id.indexOf("?");
3030
3296
  const path = queryStart === -1 ? id : id.slice(0, queryStart);
@@ -3032,7 +3298,7 @@ function isRouteOrShellFile(id, dirs) {
3032
3298
  const extIndex = path.lastIndexOf(".");
3033
3299
  if (extIndex === -1) return false;
3034
3300
  const ext = path.slice(extIndex);
3035
- if (!ROUTE_FILE_EXTENSIONS.has(ext)) return false;
3301
+ if (!extensions.has(ext)) return false;
3036
3302
  const normalized = toPosixPath(path);
3037
3303
  return dirs.some((dir) => normalized.startsWith(dir));
3038
3304
  }