@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.
@@ -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
- * Comments and strings are masked first, so prose or a string literal
172
- * mentioning the name cannot produce a false positive.
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 bindingIncludesLoader(node) {
206
+ function bindingIncludesName(node, exportName) {
199
207
  if (!isSyntaxNode(node)) return false;
200
- if (node.type === "Identifier") return node.name === "loader";
201
- if (node.type === "AssignmentPattern") return bindingIncludesLoader(node.left);
202
- if (node.type === "RestElement") return bindingIncludesLoader(node.argument);
203
- if (node.type === "ArrayPattern") return Array.isArray(node.elements) && node.elements.some(bindingIncludesLoader);
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" ? bindingIncludesLoader(property.argument) : bindingIncludesLoader(property.value);
214
+ return property.type === "RestElement" ? bindingIncludesName(property.argument, exportName) : bindingIncludesName(property.value, exportName);
207
215
  });
208
216
  return false;
209
217
  }
210
- function exportedNameIsLoader(node) {
218
+ function exportedNameMatches(node, exportName) {
211
219
  if (!isSyntaxNode(node)) return false;
212
- if (node.type === "Identifier") return node.name === "loader";
213
- if (node.type === "StringLiteral") return node.value === "loader";
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" && exportedNameIsLoader(specifier.exported))) return true;
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) && bindingIncludesLoader(declarator.id))) return true;
239
- } else if (bindingIncludesLoader(declaration.id)) return true;
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, createRouteStaticPathsHints as c, extensionGlob as d, normalizeAdditionalExtensions as f, scanPagesDirectory as i, DEFAULT_ROUTE_EXTENSIONS as l, generatePagesManifestSource as n, createRouteHeadHints as o, withAdditionalExtensions as p, generateRoutesFile as r, createRouteLoaderHints as s, filePathToRoutePath as t, LEGACY_BARE_ROUTE_EXTENSIONS as u };
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 };
@@ -12,6 +12,7 @@ interface ScannedPage {
12
12
  hasRevalidateExport?: boolean;
13
13
  hasLoader?: boolean;
14
14
  hasHead?: boolean;
15
+ hasHeaders?: boolean;
15
16
  }
16
17
  interface PagesRouterOptions {
17
18
  pagesDir: string;
@@ -1,2 +1,2 @@
1
- import { a as sortRoutes, i as scanPagesDirectory, n as generatePagesManifestSource, r as generateRoutesFile, t as filePathToRoutePath } from "./pages-router-KcCRkhnf.mjs";
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.10.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.1",
50
- "@pracht/capabilities": "0.2.0",
51
- "@pracht/core": "0.15.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
+ }