@pracht/vite-plugin 0.7.2 → 0.7.3

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.
Files changed (2) hide show
  1. package/dist/index.mjs +74 -52
  2. package/package.json +4 -4
package/dist/index.mjs CHANGED
@@ -5,8 +5,8 @@ import preact from "@preact/preset-vite";
5
5
  import { dirname, extname, join, resolve } from "node:path";
6
6
  import { loadEnv, parseAst } from "vite";
7
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,32 @@ 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 counterpart in
1225
+ * the client module graph, meaning client HMR can never deliver its change.
1226
+ */
1227
+ function isServerOnlyModuleFile(server, file) {
1228
+ const environments = server.environments;
1229
+ const client = environments?.client;
1230
+ if (!client) return false;
1231
+ if (hasModules(client, file)) return false;
1232
+ for (const [name, environment] of Object.entries(environments ?? {})) {
1233
+ if (name === "client" || !environment) continue;
1234
+ if (hasModules(environment, file)) return true;
1235
+ }
1236
+ return false;
1237
+ }
1238
+ /** Reload open pages when `file` can only reach them through the server. */
1239
+ function sendServerOnlyFullReload(server, file) {
1240
+ if (!isServerOnlyModuleFile(server, file)) return false;
1241
+ server.environments?.client?.hot?.send({ type: "full-reload" });
1242
+ return true;
1243
+ }
1244
+ function hasModules(environment, file) {
1245
+ return (environment.moduleGraph.getModulesByFile(file)?.size ?? 0) > 0;
1246
+ }
1247
+ //#endregion
1222
1248
  //#region src/plugin-assets.ts
1223
1249
  const PRACHT_CLIENT_MODULE_ID = "virtual:pracht/client";
1224
1250
  const PRACHT_SERVER_MODULE_ID = "virtual:pracht/server";
@@ -1415,57 +1441,37 @@ function extractCapabilities(options = {}, root = process.cwd()) {
1415
1441
  return extractCapabilityMetadata(name, file, source);
1416
1442
  });
1417
1443
  }
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;
1444
+ /**
1445
+ * Absolute paths of the capability modules the manifest registers.
1446
+ *
1447
+ * The client-import guard uses this rather than a `capabilitiesDir` prefix
1448
+ * test: registration is what makes a module server-only, and the manifest may
1449
+ * point anywhere. A directory test both misses capabilities registered from
1450
+ * elsewhere and wrongly rejects ordinary co-located files (shared constants,
1451
+ * types) that happen to sit in the capability folder.
1452
+ *
1453
+ * Returns an empty list when the manifest cannot be read or parsed — the
1454
+ * virtual-module generation raises its own precise error for those, and
1455
+ * guessing here would turn one clear failure into two confusing ones.
1456
+ */
1457
+ function resolveCapabilityModulePaths(options = {}, root = process.cwd()) {
1458
+ const resolved = resolveOptions(options);
1459
+ if (resolved.pagesDir) return [];
1460
+ const appFileAbs = resolve(root, resolved.appFile.replace(/^\//, ""));
1461
+ let manifestSource;
1462
+ try {
1463
+ manifestSource = readFileSync(appFileAbs, "utf-8");
1464
+ } catch {
1465
+ return [];
1460
1466
  }
1467
+ const appDir = dirname(appFileAbs);
1468
+ return extractCapabilityRegistrations(manifestSource).map(({ file }) => file.startsWith("/") ? resolve(root, file.replace(/^\//, "")) : resolve(appDir, file));
1469
+ }
1470
+ function extractCapabilityMetadata(name, file, source) {
1461
1471
  return {
1462
1472
  name,
1463
1473
  file,
1464
- description,
1465
- effect,
1466
- httpPath,
1467
- webmcp,
1468
- inputSchema
1474
+ ...extractCapabilityProjection(name, source, (detail) => `[pracht] Capability ${JSON.stringify(name)} (${file}) ${detail}`)
1469
1475
  };
1470
1476
  }
1471
1477
  /**
@@ -1643,9 +1649,6 @@ function hasWebmcpCapabilities(options = {}, root = process.cwd()) {
1643
1649
  return true;
1644
1650
  }
1645
1651
  }
1646
- function isPlainObject(value) {
1647
- return typeof value === "object" && value !== null && !Array.isArray(value);
1648
- }
1649
1652
  //#endregion
1650
1653
  //#region src/plugin-codegen.ts
1651
1654
  const ROUTE_MODULE_EXTENSIONS = new Set([
@@ -2484,6 +2487,7 @@ function pracht(options = {}) {
2484
2487
  const isPagesMode = !!resolved.pagesDir;
2485
2488
  let root = process.cwd();
2486
2489
  let routeFileDirs = [];
2490
+ let capabilityModulePaths = /* @__PURE__ */ new Set();
2487
2491
  if (isPagesMode && options.appFile) console.warn("[pracht] Both `pagesDir` and `appFile` are set. `pagesDir` takes precedence — `appFile` will be ignored.");
2488
2492
  let isBuild = false;
2489
2493
  const prachtPlugin = {
@@ -2519,6 +2523,7 @@ function pracht(options = {}) {
2519
2523
  root = config.root;
2520
2524
  isBuild = config.command === "build";
2521
2525
  routeFileDirs = computeRouteFileDirs(root, resolved);
2526
+ capabilityModulePaths = new Set(resolveCapabilityModulePaths(resolved, root).map((path) => toPosixPath(path)));
2522
2527
  },
2523
2528
  resolveId(id, importer, resolveIdOptions) {
2524
2529
  if (isIslandsClientModule(id)) return PRACHT_ISLANDS_CLIENT_MODULE_ID;
@@ -2580,6 +2585,7 @@ function pracht(options = {}) {
2580
2585
  if (isPagesMode && relative.startsWith(resolved.pagesDir)) {
2581
2586
  clearPagesAppSourceCache();
2582
2587
  invalidateVirtualModules(server);
2588
+ sendServerOnlyFullReload(server, file);
2583
2589
  return;
2584
2590
  }
2585
2591
  if (!isPagesMode && relative === resolved.appFile) {
@@ -2617,12 +2623,14 @@ function pracht(options = {}) {
2617
2623
  if (capabilityMod) server.moduleGraph.invalidateModule(capabilityMod);
2618
2624
  }
2619
2625
  }
2626
+ sendServerOnlyFullReload(server, file);
2620
2627
  }
2621
2628
  };
2622
2629
  const clientModuleTransformPlugin = {
2623
2630
  name: "pracht:client-module-transform",
2624
2631
  enforce: "post",
2625
2632
  transform(code, id, transformOptions) {
2633
+ 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
2634
  if (!(isPrachtClientModuleId(id) || !transformOptions?.ssr && isRouteOrShellFile(id, routeFileDirs))) return null;
2627
2635
  const transformed = stripServerOnlyExportsForClient(code, id);
2628
2636
  if (transformed === code) return null;
@@ -2754,6 +2762,20 @@ const ROUTE_FILE_EXTENSIONS = new Set([
2754
2762
  function computeRouteFileDirs(root, resolved) {
2755
2763
  return (resolved.pagesDir ? [resolved.pagesDir] : [resolved.routesDir, resolved.shellsDir]).map((dir) => resolveConfigPath(root, dir)).map(withTrailingSep);
2756
2764
  }
2765
+ /**
2766
+ * Whether `id` is one of the capability modules the manifest registers.
2767
+ * Matching the registered set rather than a directory keeps ordinary files that
2768
+ * merely live beside capabilities importable, and still catches a capability
2769
+ * registered from anywhere else in the project. Extension-agnostic, because the
2770
+ * comparison is against paths the manifest already resolved.
2771
+ */
2772
+ function isCapabilityModule(id, capabilityModulePaths) {
2773
+ if (capabilityModulePaths.size === 0) return false;
2774
+ const queryStart = id.indexOf("?");
2775
+ const path = queryStart === -1 ? id : id.slice(0, queryStart);
2776
+ if (path.startsWith("\0") || path.startsWith("virtual:")) return false;
2777
+ return capabilityModulePaths.has(toPosixPath(path));
2778
+ }
2757
2779
  function isRouteOrShellFile(id, dirs) {
2758
2780
  if (dirs.length === 0) return false;
2759
2781
  const queryStart = id.indexOf("?");
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.3",
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,9 +44,9 @@
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",
47
+ "@pracht/adapter-node": "0.3.6",
48
+ "@pracht/capabilities": "0.1.1",
49
+ "@pracht/core": "0.11.3",
50
50
  "@pracht/preact-ssr-precompile": "0.1.2"
51
51
  },
52
52
  "peerDependencies": {