@pracht/vite-plugin 0.7.2 → 0.7.4

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,12 +1,12 @@
1
- import { i as scanPagesDirectory, n as generatePagesManifestSource, o as createRouteLoaderHints } from "./pages-router-BsVlzz-e.mjs";
1
+ import { i as scanPagesDirectory, n as generatePagesManifestSource, o as createRouteLoaderHints } from "./pages-router-CStJY2Zp.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { preactSsrPrecompile } from "@pracht/preact-ssr-precompile";
4
4
  import preact from "@preact/preset-vite";
5
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
5
6
  import { dirname, extname, join, resolve } from "node:path";
6
7
  import { loadEnv, parseAst } from "vite";
7
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
8
- import { CAPABILITY_SETTLED_EVENT, CAPABILITY_TRANSPORT_HEADER, CONFIRMATION_HEADER, capabilityHttpPath, isValidCapabilityHttpPath } from "@pracht/capabilities";
9
- import { evaluateLiteral, extractCapabilityRegistrations, extractDefineCapabilityArgs, scanTopLevelProperties } from "@pracht/capabilities/static";
8
+ import { CAPABILITY_SETTLED_EVENT, CAPABILITY_TRANSPORT_HEADER, CONFIRMATION_HEADER } from "@pracht/capabilities";
9
+ import { extractCapabilityProjection, extractCapabilityRegistrations } from "@pracht/capabilities/static";
10
10
  import { createNodeServerEntryModule } from "@pracht/adapter-node";
11
11
  import { resolveRegistryModule } from "@pracht/core";
12
12
  //#region src/client-module-query.ts
@@ -1219,6 +1219,35 @@ function createEnvSafetyPlugin(envSafety) {
1219
1219
  };
1220
1220
  }
1221
1221
  //#endregion
1222
+ //#region src/hot-update-reload.ts
1223
+ /**
1224
+ * True when `file` participates in server rendering but has no runtime
1225
+ * counterpart in the client module graph, meaning client HMR can never deliver
1226
+ * its change. File-only asset entries created by content scanners are watchers,
1227
+ * not browser modules, so they do not make an update client-reachable.
1228
+ */
1229
+ function isServerOnlyModuleFile(server, file) {
1230
+ const environments = server.environments;
1231
+ const client = environments?.client;
1232
+ if (!client) return false;
1233
+ if (hasRuntimeModules(client, file)) return false;
1234
+ for (const [name, environment] of Object.entries(environments ?? {})) {
1235
+ if (name === "client" || !environment) continue;
1236
+ if (hasRuntimeModules(environment, file)) return true;
1237
+ }
1238
+ return false;
1239
+ }
1240
+ /** Reload open pages when `file` can only reach them through the server. */
1241
+ function sendServerOnlyFullReload(server, file) {
1242
+ if (!isServerOnlyModuleFile(server, file)) return false;
1243
+ server.environments?.client?.hot?.send({ type: "full-reload" });
1244
+ return true;
1245
+ }
1246
+ function hasRuntimeModules(environment, file) {
1247
+ for (const module of environment.moduleGraph.getModulesByFile(file) ?? []) if (module.type !== "asset") return true;
1248
+ return false;
1249
+ }
1250
+ //#endregion
1222
1251
  //#region src/plugin-assets.ts
1223
1252
  const PRACHT_CLIENT_MODULE_ID = "virtual:pracht/client";
1224
1253
  const PRACHT_SERVER_MODULE_ID = "virtual:pracht/server";
@@ -1415,57 +1444,37 @@ function extractCapabilities(options = {}, root = process.cwd()) {
1415
1444
  return extractCapabilityMetadata(name, file, source);
1416
1445
  });
1417
1446
  }
1418
- function extractCapabilityMetadata(name, file, source) {
1419
- const args = extractDefineCapabilityArgs(source);
1420
- if (!args) throw new Error(`[pracht] Capability "${name}" (${file}) does not contain a defineCapability({ ... }) call the build can analyze.`);
1421
- const properties = scanTopLevelProperties(args);
1422
- const exposeText = properties.get("expose");
1423
- if (!exposeText) return {
1424
- name,
1425
- file,
1426
- description: "",
1427
- effect: null,
1428
- httpPath: null,
1429
- webmcp: false,
1430
- inputSchema: null
1431
- };
1432
- const expose = evaluateLiteral(exposeText);
1433
- if (!isPlainObject(expose)) throw new Error(`[pracht] Capability "${name}" (${file}): "expose" must be an inline object literal so the client projection can be generated at build time.`);
1434
- const http = expose.http;
1435
- let httpPath = null;
1436
- if (http === true) httpPath = capabilityHttpPath(name);
1437
- else if (isPlainObject(http)) httpPath = typeof http.path === "string" ? http.path : capabilityHttpPath(name);
1438
- if (httpPath && !isValidCapabilityHttpPath(httpPath)) throw new Error(`[pracht] Capability "${name}" (${file}): HTTP exposure "path" must be an exact same-origin pathname starting with "/".`);
1439
- const webmcp = expose.webmcp === true;
1440
- if (webmcp && !httpPath) throw new Error(`[pracht] Capability "${name}" (${file}): expose.webmcp requires expose.http.`);
1441
- let description = "";
1442
- const descriptionText = properties.get("description");
1443
- if (descriptionText) {
1444
- const value = evaluateLiteral(descriptionText);
1445
- if (typeof value === "string") description = value;
1446
- }
1447
- let effect = null;
1448
- const effectText = properties.get("effect");
1449
- if (effectText) {
1450
- const value = evaluateLiteral(effectText);
1451
- if (typeof value === "string") effect = value;
1452
- }
1453
- if (httpPath && effect !== "read" && effect !== "write" && effect !== "destructive") throw new Error(`[pracht] Capability "${name}" (${file}) is exposed via HTTP, but its "effect" could not be extracted at build time. HTTP-exposed capabilities must declare "effect" as an inline "read", "write", or "destructive" string literal.`);
1454
- let inputSchema = null;
1455
- if (webmcp) {
1456
- const inputText = properties.get("input");
1457
- const value = inputText ? evaluateLiteral(inputText) : void 0;
1458
- if (!isPlainObject(value)) throw new Error(`[pracht] Capability "${name}" (${file}) is exposed via WebMCP, but its "input" schema could not be extracted at build time. WebMCP-exposed capabilities must declare their input schema as an inline object literal.`);
1459
- inputSchema = value;
1447
+ /**
1448
+ * Absolute paths of the capability modules the manifest registers.
1449
+ *
1450
+ * The client-import guard uses this rather than a `capabilitiesDir` prefix
1451
+ * test: registration is what makes a module server-only, and the manifest may
1452
+ * point anywhere. A directory test both misses capabilities registered from
1453
+ * elsewhere and wrongly rejects ordinary co-located files (shared constants,
1454
+ * types) that happen to sit in the capability folder.
1455
+ *
1456
+ * Returns an empty list when the manifest cannot be read or parsed — the
1457
+ * virtual-module generation raises its own precise error for those, and
1458
+ * guessing here would turn one clear failure into two confusing ones.
1459
+ */
1460
+ function resolveCapabilityModulePaths(options = {}, root = process.cwd()) {
1461
+ const resolved = resolveOptions(options);
1462
+ if (resolved.pagesDir) return [];
1463
+ const appFileAbs = resolve(root, resolved.appFile.replace(/^\//, ""));
1464
+ let manifestSource;
1465
+ try {
1466
+ manifestSource = readFileSync(appFileAbs, "utf-8");
1467
+ } catch {
1468
+ return [];
1460
1469
  }
1470
+ const appDir = dirname(appFileAbs);
1471
+ return extractCapabilityRegistrations(manifestSource).map(({ file }) => file.startsWith("/") ? resolve(root, file.replace(/^\//, "")) : resolve(appDir, file));
1472
+ }
1473
+ function extractCapabilityMetadata(name, file, source) {
1461
1474
  return {
1462
1475
  name,
1463
1476
  file,
1464
- description,
1465
- effect,
1466
- httpPath,
1467
- webmcp,
1468
- inputSchema
1477
+ ...extractCapabilityProjection(name, source, (detail) => `[pracht] Capability ${JSON.stringify(name)} (${file}) ${detail}`)
1469
1478
  };
1470
1479
  }
1471
1480
  /**
@@ -1643,9 +1652,6 @@ function hasWebmcpCapabilities(options = {}, root = process.cwd()) {
1643
1652
  return true;
1644
1653
  }
1645
1654
  }
1646
- function isPlainObject(value) {
1647
- return typeof value === "object" && value !== null && !Array.isArray(value);
1648
- }
1649
1655
  //#endregion
1650
1656
  //#region src/plugin-codegen.ts
1651
1657
  const ROUTE_MODULE_EXTENSIONS = new Set([
@@ -2484,6 +2490,7 @@ function pracht(options = {}) {
2484
2490
  const isPagesMode = !!resolved.pagesDir;
2485
2491
  let root = process.cwd();
2486
2492
  let routeFileDirs = [];
2493
+ let capabilityModulePaths = /* @__PURE__ */ new Set();
2487
2494
  if (isPagesMode && options.appFile) console.warn("[pracht] Both `pagesDir` and `appFile` are set. `pagesDir` takes precedence — `appFile` will be ignored.");
2488
2495
  let isBuild = false;
2489
2496
  const prachtPlugin = {
@@ -2519,6 +2526,7 @@ function pracht(options = {}) {
2519
2526
  root = config.root;
2520
2527
  isBuild = config.command === "build";
2521
2528
  routeFileDirs = computeRouteFileDirs(root, resolved);
2529
+ capabilityModulePaths = new Set(resolveCapabilityModulePaths(resolved, root).map(canonicalFilePath));
2522
2530
  },
2523
2531
  resolveId(id, importer, resolveIdOptions) {
2524
2532
  if (isIslandsClientModule(id)) return PRACHT_ISLANDS_CLIENT_MODULE_ID;
@@ -2543,8 +2551,8 @@ function pracht(options = {}) {
2543
2551
  return null;
2544
2552
  },
2545
2553
  transform(code, id) {
2546
- const appFileAbs = resolveConfigPath(root, resolved.appFile);
2547
- if (toPosixPath(id.split("?")[0]) !== appFileAbs) return null;
2554
+ const appFileAbs = canonicalFilePath(resolveConfigPath(root, resolved.appFile));
2555
+ if (canonicalFilePath(id.split("?")[0]) !== appFileAbs) return null;
2548
2556
  const transformed = rewriteManifestCoreImports(code.replace(/\(\)\s*=>\s*import\(\s*(['"])([^'"]+)\1\s*\)/g, "$1$2$1"));
2549
2557
  if (transformed === code) return null;
2550
2558
  return {
@@ -2580,6 +2588,7 @@ function pracht(options = {}) {
2580
2588
  if (isPagesMode && relative.startsWith(resolved.pagesDir)) {
2581
2589
  clearPagesAppSourceCache();
2582
2590
  invalidateVirtualModules(server);
2591
+ sendServerOnlyFullReload(server, file);
2583
2592
  return;
2584
2593
  }
2585
2594
  if (!isPagesMode && relative === resolved.appFile) {
@@ -2617,12 +2626,14 @@ function pracht(options = {}) {
2617
2626
  if (capabilityMod) server.moduleGraph.invalidateModule(capabilityMod);
2618
2627
  }
2619
2628
  }
2629
+ sendServerOnlyFullReload(server, file);
2620
2630
  }
2621
2631
  };
2622
2632
  const clientModuleTransformPlugin = {
2623
2633
  name: "pracht:client-module-transform",
2624
2634
  enforce: "post",
2625
2635
  transform(code, id, transformOptions) {
2636
+ 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.`);
2626
2637
  if (!(isPrachtClientModuleId(id) || !transformOptions?.ssr && isRouteOrShellFile(id, routeFileDirs))) return null;
2627
2638
  const transformed = stripServerOnlyExportsForClient(code, id);
2628
2639
  if (transformed === code) return null;
@@ -2752,7 +2763,34 @@ const ROUTE_FILE_EXTENSIONS = new Set([
2752
2763
  ".tsrx"
2753
2764
  ]);
2754
2765
  function computeRouteFileDirs(root, resolved) {
2755
- return (resolved.pagesDir ? [resolved.pagesDir] : [resolved.routesDir, resolved.shellsDir]).map((dir) => resolveConfigPath(root, dir)).map(withTrailingSep);
2766
+ return (resolved.pagesDir ? [resolved.pagesDir] : [resolved.routesDir, resolved.shellsDir]).map((dir) => canonicalFilePath(resolveConfigPath(root, dir))).map(withTrailingSep);
2767
+ }
2768
+ /**
2769
+ * Whether `id` is one of the capability modules the manifest registers.
2770
+ * Matching the registered set rather than a directory keeps ordinary files that
2771
+ * merely live beside capabilities importable, and still catches a capability
2772
+ * registered from anywhere else in the project. Extension-agnostic, because the
2773
+ * comparison is against paths the manifest already resolved.
2774
+ */
2775
+ function isCapabilityModule(id, capabilityModulePaths) {
2776
+ if (capabilityModulePaths.size === 0) return false;
2777
+ const queryStart = id.indexOf("?");
2778
+ const path = queryStart === -1 ? id : id.slice(0, queryStart);
2779
+ if (path.startsWith("\0") || path.startsWith("virtual:")) return false;
2780
+ return capabilityModulePaths.has(canonicalFilePath(path));
2781
+ }
2782
+ /**
2783
+ * Match Vite's canonical module ids even when the manifest path crosses a
2784
+ * symlink (including macOS' /var -> /private/var alias). Missing paths keep
2785
+ * their lexical identity so the projection code can raise its precise missing
2786
+ * capability error later.
2787
+ */
2788
+ function canonicalFilePath(path) {
2789
+ try {
2790
+ return toPosixPath(realpathSync.native(path));
2791
+ } catch {
2792
+ return toPosixPath(path);
2793
+ }
2756
2794
  }
2757
2795
  function isRouteOrShellFile(id, dirs) {
2758
2796
  if (dirs.length === 0) return false;
@@ -1,2 +1,2 @@
1
- import { a as sortRoutes, i as scanPagesDirectory, n as generatePagesManifestSource, r as generateRoutesFile, t as filePathToRoutePath } from "./pages-router-BsVlzz-e.mjs";
1
+ import { a as sortRoutes, i as scanPagesDirectory, n as generatePagesManifestSource, r as generateRoutesFile, t as filePathToRoutePath } from "./pages-router-CStJY2Zp.mjs";
2
2
  export { filePathToRoutePath, generatePagesManifestSource, generateRoutesFile, scanPagesDirectory, sortRoutes };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pracht/vite-plugin",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Vite plugin for Pracht apps with virtual modules, dev SSR, prerendering, route inspection, and multi-adapter builds.",
5
5
  "keywords": [
6
6
  "pracht",
@@ -44,10 +44,10 @@
44
44
  "dependencies": {
45
45
  "@preact/preset-vite": "^2.10.5",
46
46
  "@prefresh/vite": "^2.0.0",
47
- "@pracht/adapter-node": "0.3.5",
48
- "@pracht/capabilities": "0.1.0",
49
- "@pracht/core": "0.11.2",
50
- "@pracht/preact-ssr-precompile": "0.1.2"
47
+ "@pracht/core": "0.11.3",
48
+ "@pracht/adapter-node": "0.3.6",
49
+ "@pracht/preact-ssr-precompile": "0.1.2",
50
+ "@pracht/capabilities": "0.1.1"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "vite": "^8.0.0"
@@ -1,5 +1,5 @@
1
- import { basename, extname, join, relative } from "node:path";
2
1
  import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { basename, extname, join, relative } from "node:path";
3
3
  //#region src/route-loader-hints.ts
4
4
  const ROUTE_EXTENSIONS = new Set([
5
5
  ".tsx",