@stacksjs/stx 0.2.175 → 0.2.177

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 (59) hide show
  1. package/dist/a11y.d.ts +72 -2
  2. package/dist/a11y.js +25 -10
  3. package/dist/builtins/attrs.d.ts +19 -0
  4. package/dist/builtins/attrs.js +18 -0
  5. package/dist/builtins/safe-image.js +2 -1
  6. package/dist/builtins/stx-image.js +2 -1
  7. package/dist/builtins/stx-link.js +2 -1
  8. package/dist/cli-flags.d.ts +34 -0
  9. package/dist/cli-flags.js +20 -0
  10. package/dist/cli.js +1132 -895
  11. package/dist/client-script.js +2 -1
  12. package/dist/component-registry.d.ts +9 -0
  13. package/dist/component-renderer.js +20 -1
  14. package/dist/composables/use-fetch.d.ts +6 -1
  15. package/dist/composables/use-fetch.js +40 -18
  16. package/dist/composables/use-query.d.ts +7 -2
  17. package/dist/cookie-serialize.d.ts +40 -0
  18. package/dist/cookie-serialize.js +18 -0
  19. package/dist/craft.js +670 -442
  20. package/dist/crosswind-config.d.ts +13 -0
  21. package/dist/crosswind-config.js +6 -2
  22. package/dist/dev-server/crosswind.js +2 -1
  23. package/dist/edge-runtime.d.ts +2 -9
  24. package/dist/edge-runtime.js +2 -16
  25. package/dist/includes.js +8 -4
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.js +3 -0
  28. package/dist/page-action.d.ts +10 -20
  29. package/dist/page-action.js +33 -9
  30. package/dist/process.js +2 -1
  31. package/dist/production-server.js +8 -7
  32. package/dist/props.d.ts +51 -8
  33. package/dist/pwa.js +637 -409
  34. package/dist/reactive.js +2 -2
  35. package/dist/resolve-stx-targets.d.ts +49 -0
  36. package/dist/resolve-stx-targets.js +38 -0
  37. package/dist/runtime-globals.d.ts +24 -0
  38. package/dist/runtime-globals.js +16 -0
  39. package/dist/runtime-injection.d.ts +36 -0
  40. package/dist/runtime-injection.js +14 -0
  41. package/dist/server-bindings.js +3 -0
  42. package/dist/signal-processing.js +38 -7
  43. package/dist/signals.js +157 -35
  44. package/dist/ssg.d.ts +23 -0
  45. package/dist/ssg.js +615 -383
  46. package/dist/ssr.js +2 -16
  47. package/dist/stx-virtual-ts.d.ts +72 -4
  48. package/dist/stx-virtual-ts.js +111 -5
  49. package/dist/stx.d.ts +66 -7
  50. package/dist/template-hydrator.d.ts +1 -1
  51. package/dist/template-hydrator.js +6 -6
  52. package/dist/theme-tokens.d.ts +88 -0
  53. package/dist/theme-tokens.js +71 -0
  54. package/dist/typecheck.d.ts +6 -1
  55. package/dist/typecheck.js +102 -67
  56. package/dist/types/config-types.d.ts +24 -0
  57. package/dist/utils.js +5 -4
  58. package/package.json +5 -5
  59. package/stx.d.ts +66 -7
@@ -46,6 +46,19 @@ export interface MergedCrosswindConfig {
46
46
  includePreflight: boolean;
47
47
  /** Honours the user's `minify` key (default false). */
48
48
  minify: boolean;
49
+ /**
50
+ * The `:root` / `.dark` declarations backing the role tokens (#1930).
51
+ *
52
+ * Returned rather than folded into `preflights` because a project can set
53
+ * `preflight: false`, and the tokens are not preflight — they are the values
54
+ * the generated utilities resolve against. Callers prepend this to the CSS
55
+ * they emit.
56
+ *
57
+ * Missing it degrades rather than breaks: every token carries its light value
58
+ * as the `var()` fallback, so light mode stays correct and dark mode reverts
59
+ * to the light colour.
60
+ */
61
+ tokenCSS: string;
49
62
  }
50
63
  /**
51
64
  * Merge a project's crosswind config over the package defaults.
@@ -1,3 +1,4 @@
1
+ import { semanticColors, semanticTokenCSS } from "./theme-tokens.js";
1
2
  function isPlainObject(value) {
2
3
  return typeof value === "object" && value !== null && !Array.isArray(value);
3
4
  }
@@ -8,7 +9,9 @@ export function deepMergeThemes(target, source) {
8
9
  return out;
9
10
  }
10
11
  export function mergeCrosswindConfig(base = {}, user = {}) {
11
- const baseTheme = base.theme || {}, userTheme = user.theme || {}, { extend: userExtend, ...userThemeKeys } = userTheme, { extend: baseExtend, ...baseThemeKeys } = baseTheme, theme = deepMergeThemes(baseThemeKeys, userThemeKeys), extend = deepMergeThemes(baseExtend || {}, userExtend || {});
12
+ const baseTheme = base.theme || {}, userTheme = user.theme || {}, { extend: userExtend, ...userThemeKeys } = userTheme, { extend: baseExtend, ...baseThemeKeys } = baseTheme, basePalette = baseThemeKeys.colors || {};
13
+ baseThemeKeys.colors = { ...basePalette, ...semanticColors(basePalette) };
14
+ const theme = deepMergeThemes(baseThemeKeys, userThemeKeys), extend = deepMergeThemes(baseExtend || {}, userExtend || {});
12
15
  if (Object.keys(extend).length > 0)
13
16
  theme.extend = extend;
14
17
  const safelist = [
@@ -35,6 +38,7 @@ export function mergeCrosswindConfig(base = {}, user = {}) {
35
38
  safelist,
36
39
  shortcuts,
37
40
  includePreflight: user.preflight !== !1,
38
- minify: user.minify === !0
41
+ minify: user.minify === !0,
42
+ tokenCSS: semanticTokenCSS(basePalette)
39
43
  };
40
44
  }
@@ -324,7 +324,7 @@ export async function generateCrosswindCSS(htmlContent, appDir) {
324
324
  configFingerprint = "unhashable";
325
325
  }
326
326
  }
327
- const baseConfig = hw.defaultConfig || hw.config, userConfig = cachedConfig || {}, merged = mergeCrosswindConfig(baseConfig, userConfig), { safelist, includePreflight, minify } = merged, crosswindConfig = merged.config, generator = new hw.CSSGenerator(crosswindConfig);
327
+ const baseConfig = hw.defaultConfig || hw.config, userConfig = cachedConfig || {}, merged = mergeCrosswindConfig(baseConfig, userConfig), { safelist, includePreflight, minify, tokenCSS } = merged, crosswindConfig = merged.config, generator = new hw.CSSGenerator(crosswindConfig);
328
328
  for (const cls of safelist)
329
329
  generator.generate(cls);
330
330
  for (const className of classes)
@@ -374,6 +374,7 @@ export async function generateCrosswindCSS(htmlContent, appDir) {
374
374
  css += `
375
375
  @media (prefers-color-scheme: dark) { .dark .${name} { ${darkDecls.join(" ")} } }`;
376
376
  }
377
+ css = tokenCSS + css;
377
378
  setLruCache(cacheKey, css);
378
379
  writeDiskCache(cacheKey, css);
379
380
  return css;
@@ -1,3 +1,4 @@
1
+ import { type SetCookieOptions } from './cookie-serialize';
1
2
  /**
2
3
  * Detect the current edge runtime platform.
3
4
  */
@@ -63,15 +64,7 @@ export declare function parseCookies(request: Request): Record<string, string>;
63
64
  /**
64
65
  * Create a Set-Cookie header value.
65
66
  */
66
- export declare function createCookie(name: string, value: string, options?: {
67
- maxAge?: number
68
- expires?: Date
69
- path?: string
70
- domain?: string
71
- secure?: boolean
72
- httpOnly?: boolean
73
- sameSite?: 'Strict' | 'Lax' | 'None'
74
- }): string;
67
+ export declare function createCookie(name: string, value: string, options?: SetCookieOptions): string;
75
68
  /**
76
69
  * JSON response helper.
77
70
  */
@@ -1,3 +1,4 @@
1
+ import { serializeSetCookie } from "./cookie-serialize.js";
1
2
  import nodeProcess from "node:process";
2
3
  export function detectRuntime() {
3
4
  if (typeof globalThis < "u" && "caches" in globalThis && "CF" in globalThis)
@@ -500,22 +501,7 @@ export function parseCookies(request) {
500
501
  return cookies;
501
502
  }
502
503
  export function createCookie(name, value, options) {
503
- let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
504
- if (options?.maxAge !== void 0)
505
- cookie += `; Max-Age=${options.maxAge}`;
506
- if (options?.expires)
507
- cookie += `; Expires=${options.expires.toUTCString()}`;
508
- if (options?.path)
509
- cookie += `; Path=${options.path}`;
510
- if (options?.domain)
511
- cookie += `; Domain=${options.domain}`;
512
- if (options?.secure)
513
- cookie += "; Secure";
514
- if (options?.httpOnly)
515
- cookie += "; HttpOnly";
516
- if (options?.sameSite)
517
- cookie += `; SameSite=${options.sameSite}`;
518
- return cookie;
504
+ return serializeSetCookie(name, value, options);
519
505
  }
520
506
  export function jsonResponse(data, init) {
521
507
  const headers = new Headers(init?.headers);
package/dist/includes.js CHANGED
@@ -512,14 +512,18 @@ export async function processIncludes(template, context, filePath, options, depe
512
512
  }
513
513
  const currentFilePath = path.resolve(filePath);
514
514
  includeStack.add(currentFilePath);
515
+ function includeFailure(includePath, message, templateStr, offsetPos) {
516
+ options.onIncludeError?.({ includePath, templatePath: filePath, message, partialsDir });
517
+ return createDetailedErrorMessage("Include", message, filePath, templateStr, offsetPos);
518
+ }
515
519
  async function processIncludeHelper(includePath, localVars = {}, templateStr, offsetPos) {
516
520
  const includeFilePath = resolvePath(includePath, partialsDir, filePath);
517
521
  if (!includeFilePath)
518
- return createDetailedErrorMessage("Include", `Could not resolve path for include: ${includePath}`, filePath, templateStr, offsetPos);
522
+ return includeFailure(includePath, `Could not resolve path for include: ${includePath}`, templateStr, offsetPos);
519
523
  const resolvedIncludePath = path.resolve(includeFilePath);
520
524
  if (includeStack.has(resolvedIncludePath)) {
521
525
  const chain = [...includeStack, resolvedIncludePath].map((p) => path.basename(p)).join(" -> ");
522
- return createDetailedErrorMessage("Include", `Circular include detected: ${chain}`, filePath, templateStr, offsetPos);
526
+ return includeFailure(includePath, `Circular include detected: ${chain}`, templateStr, offsetPos);
523
527
  }
524
528
  try {
525
529
  dependencies.add(includeFilePath);
@@ -529,7 +533,7 @@ export async function processIncludes(template, context, filePath, options, depe
529
533
  partialContent = await Bun.file(includeFilePath).text();
530
534
  } catch (error) {
531
535
  const errorMessage = error instanceof Error ? error.message : String(error);
532
- return createDetailedErrorMessage("Include", `Error loading include file ${includePath}: ${errorMessage}`, filePath, templateStr, offsetPos);
536
+ return includeFailure(includePath, `Error loading include file ${includePath}: ${errorMessage}`, templateStr, offsetPos);
533
537
  }
534
538
  if (partialContent)
535
539
  partialContent = partialContent.replace(/\{\{--[\s\S]*?--\}\}/g, "");
@@ -641,7 +645,7 @@ export async function processIncludes(template, context, filePath, options, depe
641
645
  return processedContent;
642
646
  } catch (error) {
643
647
  const errorMessage = error instanceof Error ? error.message : String(error);
644
- return createDetailedErrorMessage("Include", `Error processing include ${includePath}: ${errorMessage}`, filePath, templateStr, offsetPos);
648
+ return includeFailure(includePath, `Error processing include ${includePath}: ${errorMessage}`, templateStr, offsetPos);
645
649
  }
646
650
  }
647
651
  const includePattern = /@include\s*\(/g;
package/dist/index.d.ts CHANGED
@@ -204,6 +204,9 @@ export * from './reactive-bindings';
204
204
  export * from './client-helpers';
205
205
  export * from './client-script';
206
206
  export * from './strip-literals';
207
+ export * from './resolve-stx-targets';
208
+ export * from './cookie-serialize';
209
+ export * from './cli-flags';
207
210
  export * from './component-hmr';
208
211
  export * from './hydration';
209
212
  // ssg moved to stx/ssg
package/dist/index.js CHANGED
@@ -113,6 +113,9 @@ export * from "./reactive-bindings.js";
113
113
  export * from "./client-helpers.js";
114
114
  export * from "./client-script.js";
115
115
  export * from "./strip-literals.js";
116
+ export * from "./resolve-stx-targets.js";
117
+ export * from "./cookie-serialize.js";
118
+ export * from "./cli-flags.js";
116
119
  export * from "./component-hmr.js";
117
120
  export * from "./hydration.js";
118
121
  export * from "./ssr.js";
@@ -1,3 +1,4 @@
1
+ import { type SetCookieOptions } from './cookie-serialize';
1
2
  /**
2
3
  * Parse a form submission body into a plain object.
3
4
  *
@@ -36,26 +37,7 @@ export declare function runPageAction(context: Record<string, any>, options?: Ru
36
37
  * 303 rather than 302 deliberately: it is the status that makes a browser
37
38
  * follow up with a GET, so a reload or a Back does not resubmit the form.
38
39
  */
39
- export declare function actionRedirectResponse(to: string): Response;
40
- /**
41
- * A page handling its own form submission (stacksjs/stx#1847).
42
- *
43
- * A page that declares `action` in its `<script server>` block receives its own
44
- * non-GET request. Whatever the action returns is merged into the render
45
- * context, so `errors` and `values` repopulate the very same template that drew
46
- * the form — one handler for the no-JS submit and the enhanced one, which is
47
- * the half of the Laravel/Remix/SvelteKit contract stx was missing.
48
- *
49
- * ## Why this is a module rather than two implementations
50
- *
51
- * It shipped in `bun-plugin/src/serve.ts` only, so a form worked under
52
- * `buddy dev` and silently did nothing in production — the request fell through
53
- * to the ordinary render and answered 200 with the pre-submit markup, which is
54
- * indistinguishable from the action having run and found nothing wrong. That is
55
- * the same dev-only-correctness shape as `stx typecheck` being stricter than
56
- * the editor, and this repo has been bitten repeatedly by two hand-maintained
57
- * copies of one rule drifting apart. One module, both servers.
58
- */
40
+ export declare function actionRedirectResponse(to: string, cookies?: readonly string[]): Response;
59
41
  /**
60
42
  * Everything an action is given about the request.
61
43
  *
@@ -72,6 +54,7 @@ export declare interface PageActionContext {
72
54
  /** What running an action produced. */
73
55
  export declare interface PageActionResult {
74
56
  redirect?: string
57
+ cookies?: string[]
75
58
  ran: boolean
76
59
  }
77
60
  /** Request data a caller has already parsed, if it has. */
@@ -81,3 +64,10 @@ export declare interface RunPageActionOptions {
81
64
  params?: Record<string, string>
82
65
  cookies?: Record<string, string>
83
66
  }
67
+ /**
68
+ * A cookie an action wants set on the response.
69
+ *
70
+ * A bare string is the whole cookie for the common case; the object form adds
71
+ * the attributes a session cookie needs.
72
+ */
73
+ export type PageActionCookie = string | (SetCookieOptions & { value: string });
@@ -1,3 +1,4 @@
1
+ import { serializeSetCookie } from "./cookie-serialize.js";
1
2
  export async function parseFormBody(request) {
2
3
  const type = request.headers.get("content-type") || "", form = {}, put = (key, value) => {
3
4
  const existing = form[key];
@@ -46,19 +47,42 @@ export async function runPageAction(context, options = {}) {
46
47
  cookies: options.cookies ?? {}
47
48
  });
48
49
  if (result && typeof result === "object") {
49
- const to = result.redirect;
50
+ const returned = result, cookies = serializeActionCookies(returned.cookies), to = returned.redirect;
50
51
  if (typeof to === "string" && to)
51
- return { redirect: to, ran: !0 };
52
- Object.assign(context, result);
52
+ return { redirect: to, ran: !0, ...cookies.length > 0 ? { cookies } : {} };
53
+ const { cookies: _cookies, redirect: _redirect, ...data } = returned;
54
+ Object.assign(context, data);
55
+ if (cookies.length > 0)
56
+ return { ran: !0, cookies };
53
57
  }
54
58
  return { ran: !0 };
55
59
  }
56
- export function actionRedirectResponse(to) {
57
- return new Response(null, {
58
- status: 303,
59
- headers: {
60
- Location: to,
61
- "Cache-Control": "no-store"
60
+ function serializeActionCookies(value) {
61
+ if (!value || typeof value !== "object" || Array.isArray(value))
62
+ return [];
63
+ const headers = [];
64
+ for (const [name, entry] of Object.entries(value)) {
65
+ if (!name)
66
+ continue;
67
+ if (typeof entry === "string") {
68
+ headers.push(serializeSetCookie(name, entry));
69
+ continue;
62
70
  }
71
+ if (entry && typeof entry === "object") {
72
+ const options = entry;
73
+ if (typeof options.value !== "string")
74
+ continue;
75
+ headers.push(serializeSetCookie(name, options.value, options));
76
+ }
77
+ }
78
+ return headers;
79
+ }
80
+ export function actionRedirectResponse(to, cookies = []) {
81
+ const headers = new Headers({
82
+ Location: to,
83
+ "Cache-Control": "no-store"
63
84
  });
85
+ for (const cookie of cookies)
86
+ headers.append("Set-Cookie", cookie);
87
+ return new Response(null, { status: 303, headers });
64
88
  }
package/dist/process.js CHANGED
@@ -54,7 +54,7 @@ import { deriveLayoutGroup } from "stx-router/layout-metadata";
54
54
  import { initializeComponentClientFactories, injectComponentClientFactories } from "./component-client-factories.js";
55
55
  import { importOnce } from "./lazy-module.js";
56
56
  export { injectRouterScript } from "./runtime-injection.js";
57
- import { injectSignalsRuntime, outputNeedsSignalsRuntime, pageShipsSignalsRuntime } from "./runtime-injection.js";
57
+ import { injectSignalsRuntime, injectTooltipRuntime, outputNeedsSignalsRuntime, pageShipsSignalsRuntime } from "./runtime-injection.js";
58
58
  export { processJsonDirective, processOnceDirective } from "./misc-directives.js";
59
59
  export { validateClientScript } from "./script-validation.js";
60
60
  const PARENT_PLACEHOLDER = "<!--PARENT_CONTENT_PLACEHOLDER-->";
@@ -843,6 +843,7 @@ ${runtimeScript}`);
843
843
  }
844
844
  if (!pageShipsSignalsRuntime(output) && outputNeedsSignalsRuntime(output))
845
845
  output = await injectSignalsRuntime(output, opts);
846
+ output = injectTooltipRuntime(output);
846
847
  if (opts.csp?.enabled && opts.csp.useNonce && typeof context.cspNonce === "string")
847
848
  output = addNonceToInlineContent(output, context.cspNonce);
848
849
  return output;
@@ -165,25 +165,26 @@ export async function startProductionServer(options = {}) {
165
165
  }
166
166
  });
167
167
  try {
168
- const { html, boundaries, redirect } = await hydrateTemplateStream(compiled, {
168
+ const { html, boundaries, redirect, cookies } = await hydrateTemplateStream(compiled, {
169
169
  params,
170
170
  request,
171
171
  method: request.method
172
172
  });
173
173
  if (redirect)
174
- return actionRedirectResponse(redirect);
174
+ return actionRedirectResponse(redirect, cookies);
175
175
  if (boundaries && boundaries.length > 0) {
176
176
  const { renderStreamingPage, streamToResponse } = await import("./streaming.js");
177
177
  return streamToResponse(renderStreamingPage(html, boundaries, { timeoutMs: 30000 }), {
178
178
  headers: { "Cache-Control": "no-cache" }
179
179
  });
180
180
  }
181
- return new Response(html, {
182
- headers: {
183
- "Content-Type": "text/html",
184
- "Cache-Control": "no-cache"
185
- }
181
+ const headers = new Headers({
182
+ "Content-Type": "text/html",
183
+ "Cache-Control": "no-cache"
186
184
  });
185
+ for (const cookie of cookies ?? [])
186
+ headers.append("Set-Cookie", cookie);
187
+ return new Response(html, { headers });
187
188
  } catch (error) {
188
189
  console.error(`[stx] Hydration error for ${pathname}:`, error);
189
190
  return new Response(compiled.html, {
package/dist/props.d.ts CHANGED
@@ -1,9 +1,22 @@
1
1
  /**
2
2
  * Define component props with type safety and validation.
3
3
  *
4
+ * Takes an inline object type or a named `interface`/`type` — the named form is
5
+ * shown first because it is the one the framework scaffolds, and because every
6
+ * example here used to be inline. An inline literal has an implicit index
7
+ * signature and a named `interface` does not, so an inline-only docstring
8
+ * demonstrated the one spelling that could not fail (stacksjs/stx#1917).
9
+ *
4
10
  * @example
5
11
  * ```typescript
6
- * // Simple usage with types only
12
+ * // A named type, inline or imported — interface and type alias both work
13
+ * interface CardProps {
14
+ * title: string
15
+ * count: number
16
+ * }
17
+ * const props = defineProps<CardProps>()
18
+ *
19
+ * // Or inline
7
20
  * const props = defineProps<{
8
21
  * title: string
9
22
  * count: number
@@ -24,12 +37,12 @@
24
37
  * })
25
38
  * ```
26
39
  */
27
- export declare function defineProps<T extends Record<string, unknown>>(definitions?: PropsDefinition<T>): T;
40
+ export declare function defineProps<T extends PropsShape>(definitions?: PropsDefinition<T>): T;
28
41
  /**
29
42
  * Define props with runtime validation.
30
43
  * Use this when you need validation errors at runtime.
31
44
  */
32
- export declare function definePropsWithValidation<T extends Record<string, unknown>>(definitions: PropsDefinition<T>, options?: {
45
+ export declare function definePropsWithValidation<T extends PropsShape>(definitions: PropsDefinition<T>, options?: {
33
46
  componentName?: string
34
47
  throwOnError?: boolean
35
48
  logWarnings?: boolean
@@ -49,7 +62,7 @@ export declare function definePropsWithValidation<T extends Record<string, unkno
49
62
  * })
50
63
  * ```
51
64
  */
52
- export declare function withDefaults<T extends Record<string, unknown>>(props: T, defaults: Partial<{ [K in keyof T]: T[K] | (() => T[K]) }>): T;
65
+ export declare function withDefaults<T extends PropsShape>(props: T, defaults: Partial<{ [K in keyof T]: T[K] | (() => T[K]) }>): T;
53
66
  /**
54
67
  * Create a prop type definition for a specific type.
55
68
  */
@@ -77,7 +90,7 @@ export declare function arrayOf<T>(_itemType: PropType<T>, options?: Omit<PropOp
77
90
  /**
78
91
  * Create a prop for object with specific shape.
79
92
  */
80
- export declare function shape<T extends Record<string, unknown>>(_shape: PropsDefinition<T>, options?: Omit<PropOptions<T>, 'type'>): PropOptions<T>;
93
+ export declare function shape<T extends PropsShape>(_shape: PropsDefinition<T>, options?: Omit<PropOptions<T>, 'type'>): PropOptions<T>;
81
94
  /**
82
95
  * Process props passed to a component.
83
96
  * Called internally during component rendering.
@@ -100,7 +113,7 @@ export declare function processComponentProps(rawProps: Record<string, unknown>,
100
113
  * emit('close', undefined)
101
114
  * ```
102
115
  */
103
- export declare function defineEmits<E extends Record<string, unknown>>(): EmitFn<E>;
116
+ export declare function defineEmits<E extends PropsShape>(): EmitFn<E>;
104
117
  /**
105
118
  * Expose component methods/properties to parent.
106
119
  *
@@ -113,7 +126,7 @@ export declare function defineEmits<E extends Record<string, unknown>>(): EmitFn
113
126
  * })
114
127
  * ```
115
128
  */
116
- export declare function defineExpose<T extends Record<string, unknown>>(exposed: T): void;
129
+ export declare function defineExpose<T extends PropsShape>(exposed: T): void;
117
130
  /**
118
131
  * Generate TypeScript interface from prop definitions.
119
132
  * Useful for documentation and type generation.
@@ -151,6 +164,36 @@ export type PropType<T> = | StringConstructor
151
164
  | SymbolConstructor
152
165
  | { new (...args: unknown[]): T }
153
166
  | PropType<T>[];
167
+ /**
168
+ * The constraint every props, emits and expose generic is bounded by.
169
+ *
170
+ * `object` rather than `Record<string, unknown>`, and the difference is not
171
+ * cosmetic: **a TypeScript `interface` does not satisfy an index signature.**
172
+ * Only a type alias gets an implicit one, so
173
+ *
174
+ * interface CardProps { title: string }
175
+ * defineProps<CardProps>() // TS2344, TS2345
176
+ * type CardProps = { title: string }
177
+ * defineProps<CardProps>() // fine
178
+ *
179
+ * differ on the keyword alone. `interface` is the form the framework scaffolds
180
+ * — `storage/framework/defaults/` declares props with it 44 times and with a
181
+ * type alias zero times — so the constraint rejected the entire scaffolded
182
+ * corpus (stacksjs/stx#1917).
183
+ *
184
+ * Two things hid it. Every example in the docstrings passes an inline object
185
+ * literal, which HAS an implicit index signature and so always passed; the
186
+ * error appears only once the type is named, which is the moment it is reused
187
+ * or exported. And apps import from `stx`, which is not a package anyone can
188
+ * install — the module resolved to `any`, the constraint was never enforced,
189
+ * and the component type-checked. Correcting the specifier in one app turned 18
190
+ * "Cannot find module" errors into 36 constraint errors.
191
+ *
192
+ * Named rather than written out seven times so the reason lives in one place
193
+ * and the sites cannot drift apart. `object` still rejects `string`, `number`
194
+ * and `undefined`, which is everything this constraint has to do.
195
+ */
196
+ export type PropsShape = object;
154
197
  /** Props definition object */
155
198
  export type PropsDefinition<T> = {
156
199
  [K in keyof T]?: PropOptions<T[K]> | PropType<T[K]>
@@ -167,7 +210,7 @@ export type ExtractPropTypes<T extends PropsDefinition<unknown>> = {
167
210
  // eslint-disable-next-line pickier/no-unused-vars
168
211
  export type EventHandler<T = unknown> = (payload: T) => void;
169
212
  /** Emit function type */
170
- export type EmitFn<E extends Record<string, unknown>> = <K extends keyof E>(
213
+ export type EmitFn<E extends PropsShape> = <K extends keyof E>(
171
214
  event: K,
172
215
  payload: E[K],
173
216
  ) => void;