@pracht/vite-plugin 0.10.0 → 0.11.1
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.d.mts +83 -4
- package/dist/index.mjs +499 -56
- package/dist/{pages-router-KcCRkhnf.mjs → pages-router-MA9rOl88.mjs} +48 -18
- package/dist/pages-router.d.mts +1 -0
- package/dist/pages-router.mjs +1 -1
- package/package.json +9 -5
- package/virtual.d.ts +255 -0
|
@@ -46,6 +46,7 @@ function namedDeclarationRe(exportName) {
|
|
|
46
46
|
return new RegExp(`export\\s+(?:async\\s+)?(?:function|const|let|var)\\s+${exportName}\\b`);
|
|
47
47
|
}
|
|
48
48
|
const HEAD_DECLARATION_RE = namedDeclarationRe("head");
|
|
49
|
+
const HEADERS_DECLARATION_RE = namedDeclarationRe("headers");
|
|
49
50
|
const STATIC_PATHS_DECLARATION_RE = namedDeclarationRe("getStaticPaths");
|
|
50
51
|
const EXPORT_BLOCK_RE = /export\s*\{([^}]*)\}\s*(?:from\s*["'][^"']+["'])?/g;
|
|
51
52
|
const EXPORT_ALL_RE = /export\s+\*\s+from\b/;
|
|
@@ -168,10 +169,13 @@ function exportSpecifiersInclude(specifiers, exportName) {
|
|
|
168
169
|
* Whether `source` exports `exportName`, via a declaration, an export block,
|
|
169
170
|
* or an `export *` re-export (which could expose anything, so it counts).
|
|
170
171
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
172
|
+
* Ordinary TS/JS is parsed exactly, including string-literal export names.
|
|
173
|
+
* Custom syntaxes fall back to masked lexical detection so prose or a string
|
|
174
|
+
* literal mentioning the name cannot produce a false positive.
|
|
173
175
|
*/
|
|
174
176
|
function detectNamedExport(source, exportName, declarationRe) {
|
|
177
|
+
const parsedResult = inspectParsedModule(source, exportName);
|
|
178
|
+
if (parsedResult !== void 0) return parsedResult;
|
|
175
179
|
const analysisSource = maskCommentsAndStrings(source);
|
|
176
180
|
if (declarationRe.test(analysisSource) || variableDeclarationExports(analysisSource, exportName)) return true;
|
|
177
181
|
for (const match of analysisSource.matchAll(EXPORT_BLOCK_RE)) if (exportSpecifiersInclude(match[1], exportName)) return true;
|
|
@@ -180,6 +184,10 @@ function detectNamedExport(source, exportName, declarationRe) {
|
|
|
180
184
|
function detectHeadExport(source) {
|
|
181
185
|
return detectNamedExport(source, "head", HEAD_DECLARATION_RE);
|
|
182
186
|
}
|
|
187
|
+
/** Whether the route or shell module exports document response headers. */
|
|
188
|
+
function detectHeadersExport(source) {
|
|
189
|
+
return detectNamedExport(source, "headers", HEADERS_DECLARATION_RE);
|
|
190
|
+
}
|
|
183
191
|
/**
|
|
184
192
|
* Whether the route module exports `getStaticPaths()`.
|
|
185
193
|
*
|
|
@@ -195,25 +203,25 @@ function detectStaticPathsExport(source) {
|
|
|
195
203
|
function isSyntaxNode(value) {
|
|
196
204
|
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
197
205
|
}
|
|
198
|
-
function
|
|
206
|
+
function bindingIncludesName(node, exportName) {
|
|
199
207
|
if (!isSyntaxNode(node)) return false;
|
|
200
|
-
if (node.type === "Identifier") return node.name ===
|
|
201
|
-
if (node.type === "AssignmentPattern") return
|
|
202
|
-
if (node.type === "RestElement") return
|
|
203
|
-
if (node.type === "ArrayPattern") return Array.isArray(node.elements) && node.elements.some(
|
|
208
|
+
if (node.type === "Identifier") return node.name === exportName;
|
|
209
|
+
if (node.type === "AssignmentPattern") return bindingIncludesName(node.left, exportName);
|
|
210
|
+
if (node.type === "RestElement") return bindingIncludesName(node.argument, exportName);
|
|
211
|
+
if (node.type === "ArrayPattern") return Array.isArray(node.elements) && node.elements.some((element) => bindingIncludesName(element, exportName));
|
|
204
212
|
if (node.type === "ObjectPattern") return Array.isArray(node.properties) && node.properties.some((property) => {
|
|
205
213
|
if (!isSyntaxNode(property)) return false;
|
|
206
|
-
return property.type === "RestElement" ?
|
|
214
|
+
return property.type === "RestElement" ? bindingIncludesName(property.argument, exportName) : bindingIncludesName(property.value, exportName);
|
|
207
215
|
});
|
|
208
216
|
return false;
|
|
209
217
|
}
|
|
210
|
-
function
|
|
218
|
+
function exportedNameMatches(node, exportName) {
|
|
211
219
|
if (!isSyntaxNode(node)) return false;
|
|
212
|
-
if (node.type === "Identifier") return node.name ===
|
|
213
|
-
if (node.type === "StringLiteral") return node.value ===
|
|
220
|
+
if (node.type === "Identifier") return node.name === exportName;
|
|
221
|
+
if (node.type === "StringLiteral") return node.value === exportName;
|
|
214
222
|
return false;
|
|
215
223
|
}
|
|
216
|
-
function inspectParsedModule(source) {
|
|
224
|
+
function inspectParsedModule(source, exportName) {
|
|
217
225
|
for (const plugins of [["typescript", "jsx"], ["typescript"]]) {
|
|
218
226
|
let body;
|
|
219
227
|
try {
|
|
@@ -230,19 +238,19 @@ function inspectParsedModule(source) {
|
|
|
230
238
|
continue;
|
|
231
239
|
}
|
|
232
240
|
if (statement.type !== "ExportNamedDeclaration" || statement.exportKind === "type") continue;
|
|
233
|
-
if (Array.isArray(statement.specifiers) && statement.specifiers.some((specifier) => isSyntaxNode(specifier) && specifier.exportKind !== "type" &&
|
|
241
|
+
if (Array.isArray(statement.specifiers) && statement.specifiers.some((specifier) => isSyntaxNode(specifier) && specifier.exportKind !== "type" && exportedNameMatches(specifier.exported, exportName))) return true;
|
|
234
242
|
const declaration = statement.declaration;
|
|
235
243
|
if (!isSyntaxNode(declaration)) continue;
|
|
236
244
|
if (declaration.declare === true || declaration.type.startsWith("TS")) continue;
|
|
237
245
|
if (declaration.type === "VariableDeclaration") {
|
|
238
|
-
if (Array.isArray(declaration.declarations) && declaration.declarations.some((declarator) => isSyntaxNode(declarator) &&
|
|
239
|
-
} else if (
|
|
246
|
+
if (Array.isArray(declaration.declarations) && declaration.declarations.some((declarator) => isSyntaxNode(declarator) && bindingIncludesName(declarator.id, exportName))) return true;
|
|
247
|
+
} else if (bindingIncludesName(declaration.id, exportName)) return true;
|
|
240
248
|
}
|
|
241
249
|
return false;
|
|
242
250
|
}
|
|
243
251
|
}
|
|
244
252
|
function detectLoaderExport(source) {
|
|
245
|
-
const parsedResult = inspectParsedModule(source);
|
|
253
|
+
const parsedResult = inspectParsedModule(source, "loader");
|
|
246
254
|
if (parsedResult !== void 0) return parsedResult;
|
|
247
255
|
try {
|
|
248
256
|
const [imports, exports] = parse$1(source);
|
|
@@ -309,6 +317,26 @@ function createRouteHeadHints(routesDir, options = {}) {
|
|
|
309
317
|
}
|
|
310
318
|
return hints;
|
|
311
319
|
}
|
|
320
|
+
function createRouteHeadersHints(routesDir, options = {}) {
|
|
321
|
+
const files = [];
|
|
322
|
+
const hints = {};
|
|
323
|
+
const additionalExtensions = normalizeAdditionalExtensions(options.additionalExtensions);
|
|
324
|
+
scanRouteFiles(routesDir, files, withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, additionalExtensions));
|
|
325
|
+
for (const file of files) {
|
|
326
|
+
const extension = extname(file);
|
|
327
|
+
const hasHeaders = extension === ".md" || extension === ".mdx" || additionalExtensions.includes(extension) || detectHeadersExport(readFileSync(file, "utf-8"));
|
|
328
|
+
const relativeToRoutesDir = toPosixPath(relative(routesDir, file));
|
|
329
|
+
const routeRootPrefix = options.rootRelativePrefix?.replace(/\/$/, "");
|
|
330
|
+
const keys = /* @__PURE__ */ new Set();
|
|
331
|
+
if (options.appFileDir) {
|
|
332
|
+
const relativeToAppFile = toPosixPath(relative(options.appFileDir, file));
|
|
333
|
+
keys.add(relativeToAppFile.startsWith(".") ? relativeToAppFile : `./${relativeToAppFile}`);
|
|
334
|
+
}
|
|
335
|
+
if (routeRootPrefix) keys.add(`${routeRootPrefix}/${relativeToRoutesDir}`);
|
|
336
|
+
for (const key of keys) hints[key] = hasHeaders;
|
|
337
|
+
}
|
|
338
|
+
return hints;
|
|
339
|
+
}
|
|
312
340
|
/**
|
|
313
341
|
* Per-route-file `getStaticPaths()` presence, keyed the same way as the loader
|
|
314
342
|
* and head hints.
|
|
@@ -375,6 +403,7 @@ function scan(dir, root, pages, pageExtensions, shellExtensions, additionalExten
|
|
|
375
403
|
const revalidate = extractRevalidateSeconds(analysisSource, rel);
|
|
376
404
|
const hasLoader = detectLoaderExport(analysisSource);
|
|
377
405
|
const hasHead = ext === ".md" || ext === ".mdx" || additionalExtensions.has(ext) || detectHeadExport(analysisSource);
|
|
406
|
+
const hasHeaders = ext === ".md" || ext === ".mdx" || additionalExtensions.has(ext) || detectHeadersExport(analysisSource);
|
|
378
407
|
pages.push({
|
|
379
408
|
absolutePath: abs,
|
|
380
409
|
relativePath: rel,
|
|
@@ -387,7 +416,8 @@ function scan(dir, root, pages, pageExtensions, shellExtensions, additionalExten
|
|
|
387
416
|
revalidateSeconds: revalidate.seconds,
|
|
388
417
|
hasRevalidateExport: revalidate.present,
|
|
389
418
|
hasLoader,
|
|
390
|
-
hasHead
|
|
419
|
+
hasHead,
|
|
420
|
+
hasHeaders
|
|
391
421
|
});
|
|
392
422
|
}
|
|
393
423
|
}
|
|
@@ -585,4 +615,4 @@ function generateRoutesFile(pagesDir, outputPath, options) {
|
|
|
585
615
|
].join("\n"), "utf-8");
|
|
586
616
|
}
|
|
587
617
|
//#endregion
|
|
588
|
-
export { sortRoutes as a,
|
|
618
|
+
export { sortRoutes as a, createRouteLoaderHints as c, LEGACY_BARE_ROUTE_EXTENSIONS as d, extensionGlob as f, scanPagesDirectory as i, createRouteStaticPathsHints as l, withAdditionalExtensions as m, generatePagesManifestSource as n, createRouteHeadHints as o, normalizeAdditionalExtensions as p, generateRoutesFile as r, createRouteHeadersHints as s, filePathToRoutePath as t, DEFAULT_ROUTE_EXTENSIONS as u };
|
package/dist/pages-router.d.mts
CHANGED
package/dist/pages-router.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as sortRoutes, i as scanPagesDirectory, n as generatePagesManifestSource, r as generateRoutesFile, t as filePathToRoutePath } from "./pages-router-
|
|
1
|
+
import { a as sortRoutes, i as scanPagesDirectory, n as generatePagesManifestSource, r as generateRoutesFile, t as filePathToRoutePath } from "./pages-router-MA9rOl88.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.
|
|
3
|
+
"version": "0.11.1",
|
|
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",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"directory": "packages/vite-plugin"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"dist"
|
|
28
|
+
"dist",
|
|
29
|
+
"virtual.d.ts"
|
|
29
30
|
],
|
|
30
31
|
"type": "module",
|
|
31
32
|
"exports": {
|
|
@@ -36,6 +37,9 @@
|
|
|
36
37
|
"./pages-router": {
|
|
37
38
|
"types": "./dist/pages-router.d.mts",
|
|
38
39
|
"default": "./dist/pages-router.mjs"
|
|
40
|
+
},
|
|
41
|
+
"./virtual": {
|
|
42
|
+
"types": "./virtual.d.ts"
|
|
39
43
|
}
|
|
40
44
|
},
|
|
41
45
|
"publishConfig": {
|
|
@@ -46,9 +50,9 @@
|
|
|
46
50
|
"@preact/preset-vite": "^2.10.5",
|
|
47
51
|
"@prefresh/vite": "^2.0.0",
|
|
48
52
|
"es-module-lexer": "^1.7.0",
|
|
49
|
-
"@pracht/adapter-node": "0.4.
|
|
50
|
-
"@pracht/capabilities": "0.
|
|
51
|
-
"@pracht/core": "0.
|
|
53
|
+
"@pracht/adapter-node": "0.4.2",
|
|
54
|
+
"@pracht/capabilities": "0.3.0",
|
|
55
|
+
"@pracht/core": "0.16.0",
|
|
52
56
|
"@pracht/preact-ssr-precompile": "0.1.3"
|
|
53
57
|
},
|
|
54
58
|
"peerDependencies": {
|
package/virtual.d.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
declare module "virtual:pracht/server" {
|
|
2
|
+
const mod: { fetch: (request: Request, env: any, ctx: any) => Promise<Response> };
|
|
3
|
+
export default mod;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
declare module "virtual:pracht/client" {}
|
|
7
|
+
|
|
8
|
+
declare module "virtual:pracht/capabilities" {
|
|
9
|
+
import type {
|
|
10
|
+
CapabilityBrowserCallOptions,
|
|
11
|
+
CapabilityCallOptionsFor,
|
|
12
|
+
CapabilityInputArgs,
|
|
13
|
+
CapabilityInputFor,
|
|
14
|
+
CapabilityOutputFor,
|
|
15
|
+
HasRegisteredCapabilities,
|
|
16
|
+
HttpCapabilityName,
|
|
17
|
+
NonDestructiveCapabilityName,
|
|
18
|
+
Register,
|
|
19
|
+
} from "@pracht/core";
|
|
20
|
+
import type {
|
|
21
|
+
CapabilityEffect,
|
|
22
|
+
CapabilityEnvelope,
|
|
23
|
+
CapabilityErrorPayload,
|
|
24
|
+
CapabilityIssue,
|
|
25
|
+
} from "@pracht/capabilities";
|
|
26
|
+
|
|
27
|
+
// The envelope types are the protocol package's — re-exported so existing
|
|
28
|
+
// `import type { ... } from "virtual:pracht/capabilities"` keeps working.
|
|
29
|
+
export type { CapabilityEnvelope, CapabilityErrorPayload, CapabilityIssue };
|
|
30
|
+
|
|
31
|
+
export interface CallCapabilityOptions extends CapabilityBrowserCallOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Confirmation token for committing a destructive capability, taken from
|
|
34
|
+
* the prior call's `confirmation_required` error envelope. Sets the
|
|
35
|
+
* confirmation header for you. A destructive call must either prepare with
|
|
36
|
+
* `{ prepare: true }` or commit with this token once `pracht typegen` has
|
|
37
|
+
* registered its effect class.
|
|
38
|
+
*/
|
|
39
|
+
confirm?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Start the prepare half of a destructive call. The server returns a
|
|
42
|
+
* `confirmation_required` envelope containing the token; repeat the call
|
|
43
|
+
* with `confirm` to commit. Typed destructive calls require exactly one of
|
|
44
|
+
* `prepare: true` or `confirm`.
|
|
45
|
+
*/
|
|
46
|
+
prepare?: true;
|
|
47
|
+
/**
|
|
48
|
+
* Successful non-`read` calls revalidate the active route's data
|
|
49
|
+
* automatically; pass `false` to skip it for this call.
|
|
50
|
+
*/
|
|
51
|
+
revalidate?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Destructive calls require exactly one of `{ prepare: true }` or
|
|
56
|
+
* `{ confirm }`. `prepare` is not sent over the wire; the dispatcher uses it
|
|
57
|
+
* only to remove any confirmation token inherited through caller-supplied
|
|
58
|
+
* headers. The server is what refuses to run the unconfirmed call.
|
|
59
|
+
*/
|
|
60
|
+
type OptionsFor<TName extends string> = CapabilityCallOptionsFor<TName, CallCapabilityOptions>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* HTTP endpoints of http-exposed capabilities, keyed by capability name.
|
|
64
|
+
*
|
|
65
|
+
* Has a **null prototype**, so a capability named `toString` cannot shadow an
|
|
66
|
+
* inherited member during lookup. Index it and enumerate it as usual, but
|
|
67
|
+
* reach for `Object.hasOwn(capabilityEndpoints, name)` rather than
|
|
68
|
+
* `capabilityEndpoints.hasOwnProperty(name)` — there is no `Object.prototype`
|
|
69
|
+
* to inherit that from. TypeScript cannot express the missing prototype, so
|
|
70
|
+
* the `Record` type below overstates what is available.
|
|
71
|
+
*/
|
|
72
|
+
export const capabilityEndpoints: Record<
|
|
73
|
+
string,
|
|
74
|
+
{ method: string; path: string; effect: CapabilityEffect | null }
|
|
75
|
+
>;
|
|
76
|
+
|
|
77
|
+
interface TypedCallCapability {
|
|
78
|
+
/**
|
|
79
|
+
* Names that cannot be `destructive`. Listed first and with an optional
|
|
80
|
+
* options argument, so it is always arity-compatible with a one- or
|
|
81
|
+
* two-argument call — which makes it the signature that reports what is
|
|
82
|
+
* wrong with an unresolvable name, instead of an argument count.
|
|
83
|
+
*/
|
|
84
|
+
<TName extends NonDestructiveCapabilityName>(
|
|
85
|
+
name: TName,
|
|
86
|
+
...args: CapabilityInputArgs<TName, CallCapabilityOptions>
|
|
87
|
+
): Promise<CapabilityEnvelope<CapabilityOutputFor<TName>>>;
|
|
88
|
+
/** Possibly `destructive`: the prepare marker or the token is required. */
|
|
89
|
+
<TName extends HttpCapabilityName>(
|
|
90
|
+
name: TName,
|
|
91
|
+
input: CapabilityInputFor<TName>,
|
|
92
|
+
options: OptionsFor<TName>,
|
|
93
|
+
): Promise<CapabilityEnvelope<CapabilityOutputFor<TName>>>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface UntypedCallCapability {
|
|
97
|
+
<T = unknown>(
|
|
98
|
+
name: string,
|
|
99
|
+
input?: unknown,
|
|
100
|
+
opts?: CallCapabilityOptions,
|
|
101
|
+
): Promise<CapabilityEnvelope<T>>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Invoke an http-exposed capability from the browser via its HTTP projection.
|
|
106
|
+
* Once `pracht typegen` has registered the capability graph on
|
|
107
|
+
* `Register["capabilities"]`, the name, input, output, and confirmation
|
|
108
|
+
* requirement all come from the registration: a private capability, an
|
|
109
|
+
* unknown name, a mismatched input, or a `destructive` call without an
|
|
110
|
+
* explicit prepare/commit choice are compile errors rather than runtime
|
|
111
|
+
* envelopes.
|
|
112
|
+
*
|
|
113
|
+
* Declared as a conditionally-typed value rather than as an overload pair
|
|
114
|
+
* whose fallback `name` resolves to `never`. That fallback survived overload
|
|
115
|
+
* resolution and absorbed anything arity filtering rejected, so every
|
|
116
|
+
* mistake — including a `destructive` call that merely forgot its options —
|
|
117
|
+
* came back as `'"notes.purge"' is not assignable to 'never'`: blaming the
|
|
118
|
+
* name, never naming the cause. Here the untyped form is simply absent for a
|
|
119
|
+
* registered app, and the two typed signatures split by effect class so that
|
|
120
|
+
* an unresolvable name is always arity-compatible with the first one and gets
|
|
121
|
+
* reported as a name.
|
|
122
|
+
*
|
|
123
|
+
* A dynamic name is no longer accepted once typegen has run; assert it with
|
|
124
|
+
* `name as HttpCapabilityName` when the name genuinely comes from data, and
|
|
125
|
+
* keep in mind the runtime still answers an unknown one with
|
|
126
|
+
* `unknown_capability`.
|
|
127
|
+
*/
|
|
128
|
+
export const callCapability: HasRegisteredCapabilities extends true
|
|
129
|
+
? TypedCallCapability
|
|
130
|
+
: UntypedCallCapability;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The same calls as `callCapability`, reached as a nested object built from
|
|
134
|
+
* the dotted capability names — `capabilities.notes.search({ query })`.
|
|
135
|
+
* Private capabilities are simply absent from it. Because the members are
|
|
136
|
+
* real property accesses, a typo here reports as "Property 'serach' does not
|
|
137
|
+
* exist … Did you mean 'search'?" — which `callCapability("notes.serach")`
|
|
138
|
+
* cannot do, since a string literal argument has no such suggestion.
|
|
139
|
+
*
|
|
140
|
+
* Current typegen output declares the nested client explicitly, so each leaf
|
|
141
|
+
* carries the capability's title and description as JSDoc. The mapped type
|
|
142
|
+
* below remains the compatibility fallback for older generated files.
|
|
143
|
+
*
|
|
144
|
+
* Identical runtime path to `callCapability` (same endpoint table, same
|
|
145
|
+
* settled event, same revalidation), so nothing forks between the two.
|
|
146
|
+
*/
|
|
147
|
+
export const capabilities: PrachtCapabilityClient;
|
|
148
|
+
|
|
149
|
+
type GeneratedCapabilityClient = Register extends { capabilityClient: infer TClient }
|
|
150
|
+
? TClient
|
|
151
|
+
: never;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Dotted names expanded into nested namespaces, http-exposed only. Current
|
|
155
|
+
* typegen output supplies the explicit client (including JSDoc); older
|
|
156
|
+
* generated files use the mapped fallback. Before typegen has run, every
|
|
157
|
+
* segment stays callable with unknown input/output.
|
|
158
|
+
*/
|
|
159
|
+
export type PrachtCapabilityClient = HasRegisteredCapabilities extends true
|
|
160
|
+
? [GeneratedCapabilityClient] extends [never]
|
|
161
|
+
? CapabilityClientNode<HttpCapabilityName>
|
|
162
|
+
: GeneratedCapabilityClient
|
|
163
|
+
: Record<string, UntypedCapabilityClientNode>;
|
|
164
|
+
|
|
165
|
+
interface UntypedCapabilityClientNode {
|
|
166
|
+
<T = unknown>(input?: unknown, opts?: CallCapabilityOptions): Promise<CapabilityEnvelope<T>>;
|
|
167
|
+
[segment: string]: UntypedCapabilityClientNode;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
type CapabilityMethod<TName extends string> = (
|
|
171
|
+
...args: CapabilityInputArgs<TName, OptionsFor<TName>>
|
|
172
|
+
) => Promise<CapabilityEnvelope<CapabilityOutputFor<TName>>>;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* `Prefix` carries the already-consumed path so a leaf can look its own full
|
|
176
|
+
* dotted name back up in the flat registration map.
|
|
177
|
+
*/
|
|
178
|
+
type CapabilitySegment<
|
|
179
|
+
TAll extends string,
|
|
180
|
+
TPrefix extends string,
|
|
181
|
+
> = TAll extends `${TPrefix}${infer TRest}`
|
|
182
|
+
? TRest extends `${infer THead}.${string}`
|
|
183
|
+
? THead
|
|
184
|
+
: TRest
|
|
185
|
+
: never;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A name that is also a prefix of another (`notes` alongside `notes.search`)
|
|
189
|
+
* cannot be both a function and a namespace. The runtime builder resolves
|
|
190
|
+
* that by letting the namespace win, so the type must too — otherwise
|
|
191
|
+
* `capabilities.notes(...)` would typecheck and throw at runtime. The
|
|
192
|
+
* shadowed name stays callable through `callCapability()`, and
|
|
193
|
+
* `pracht verify` warns about it.
|
|
194
|
+
*/
|
|
195
|
+
type CapabilityClientNode<TAll extends string, TPrefix extends string = ""> = {
|
|
196
|
+
[TSeg in CapabilitySegment<TAll, TPrefix>]: [
|
|
197
|
+
Extract<TAll, `${TPrefix}${TSeg}.${string}`>,
|
|
198
|
+
] extends [never]
|
|
199
|
+
? CapabilityMethod<`${TPrefix}${TSeg}`>
|
|
200
|
+
: CapabilityClientNode<TAll, `${TPrefix}${TSeg}.`>;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Call state for a user-triggered capability call — a button, a search box, a
|
|
205
|
+
* picker. `call()` takes the same arguments as `callCapability` minus the
|
|
206
|
+
* name, and resolves to the same envelope.
|
|
207
|
+
*
|
|
208
|
+
* This is a mutation-shaped hook, not a fetch-on-render one: it dispatches
|
|
209
|
+
* when you call it, never during render. For data a page needs on load, run
|
|
210
|
+
* the capability in a `loader` with `invokeCapability()` — that result is
|
|
211
|
+
* server-rendered into the HTML and revalidates automatically after
|
|
212
|
+
* non-`read` calls, which a render-time fetch cannot do.
|
|
213
|
+
*
|
|
214
|
+
* ```tsx
|
|
215
|
+
* const search = useCapability("notes.search");
|
|
216
|
+
* await search.call({ query });
|
|
217
|
+
* // search.data / search.error / search.pending / search.reset()
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* Concurrent calls are last-one-wins: an earlier response that arrives after
|
|
221
|
+
* a later one is discarded, so typing into a search box cannot show a stale
|
|
222
|
+
* result. `data` stays visible while a follow-up call is `pending`.
|
|
223
|
+
* It also remains the most recent successful result when that follow-up fails;
|
|
224
|
+
* only `reset()` or changing the capability name clears it.
|
|
225
|
+
*/
|
|
226
|
+
export function useCapability<TName extends HttpCapabilityName>(
|
|
227
|
+
name: TName,
|
|
228
|
+
): PrachtCapabilityHook<TName>;
|
|
229
|
+
|
|
230
|
+
export interface PrachtCapabilityHook<TName extends HttpCapabilityName> {
|
|
231
|
+
call: (
|
|
232
|
+
...args: CapabilityInputArgs<TName, OptionsFor<TName>>
|
|
233
|
+
) => Promise<CapabilityEnvelope<CapabilityOutputFor<TName>>>;
|
|
234
|
+
/** Data from the most recent successful call, until `reset()`. */
|
|
235
|
+
data: CapabilityOutputFor<TName> | undefined;
|
|
236
|
+
/** Error payload from the most recent failed call, until `reset()`. */
|
|
237
|
+
error: CapabilityErrorPayload | undefined;
|
|
238
|
+
/** Whether a call is in flight. */
|
|
239
|
+
pending: boolean;
|
|
240
|
+
/** Clear `data`/`error`/`pending` and abandon any in-flight result. */
|
|
241
|
+
reset: () => void;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
declare module "virtual:pracht/webmcp" {
|
|
246
|
+
/** Registers WebMCP page tools; returns false when the API is unavailable. */
|
|
247
|
+
export function registerPrachtWebmcpTools(): boolean;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Preserve the ambient declaration shipped with Pracht's compatibility-level
|
|
251
|
+
// `.tsrx` discovery. Other custom formats provide their own declaration.
|
|
252
|
+
declare module "*.tsrx" {
|
|
253
|
+
const mod: Record<string, unknown>;
|
|
254
|
+
export = mod;
|
|
255
|
+
}
|