diff --git a/.changeset/flat-bugs-relate.md b/.changeset/flat-bugs-relate.md new file mode 100644 index 00000000..6e9b98be --- /dev/null +++ b/.changeset/flat-bugs-relate.md @@ -0,0 +1,127 @@ +--- +"astro-integration-kit": minor +--- + +Improve emitted inferred types for libraries + +For the following code: + +```ts +export const utility = defineUtility('astro:config:setup')(( + params: HookParameters<'astro:config:setup'>, + options: { name: string } +) => { + // do something +}); + +export const integration defineIntegration({ + name: 'some-utility', + setup: () => ({ + hooks: { + 'astro:config:setup': (params) => { + // do something + }, + }, + something: (it: string): string => it, + }), +}); +``` + +Previously, the emitted declarations would be: + +```ts +import * as astro from "astro"; +import { AstroIntegrationLogger } from "astro"; + +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + +type DeepPartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? DeepPartial[] + : T[P] extends object | undefined + ? DeepPartial + : T[P]; +}; + +export const utility: ( + params: { + config: astro.AstroConfig; + command: "dev" | "build" | "preview"; + isRestart: boolean; + updateConfig: ( + newConfig: DeepPartial + ) => astro.AstroConfig; + addRenderer: (renderer: astro.AstroRenderer) => void; + addWatchFile: (path: URL | string) => void; + injectScript: (stage: astro.InjectedScriptStage, content: string) => void; + injectRoute: (injectRoute: astro.InjectedRoute) => void; + addClientDirective: (directive: astro.ClientDirectiveConfig) => void; + addDevOverlayPlugin: (entrypoint: string) => void; + addDevToolbarApp: (entrypoint: astro.DevToolbarAppEntry | string) => void; + addMiddleware: (mid: astro.AstroIntegrationMiddleware) => void; + logger: AstroIntegrationLogger; + }, + options: { + name: string; + } +) => void; +export const integration: () => astro.AstroIntegration & + Prettify< + Omit< + ReturnType< + () => { + hooks: { + "astro:config:setup": (params: { + config: astro.AstroConfig; + command: "dev" | "build" | "preview"; + isRestart: boolean; + updateConfig: ( + newConfig: DeepPartial + ) => astro.AstroConfig; + addRenderer: (renderer: astro.AstroRenderer) => void; + addWatchFile: (path: URL | string) => void; + injectScript: ( + stage: astro.InjectedScriptStage, + content: string + ) => void; + injectRoute: (injectRoute: astro.InjectedRoute) => void; + addClientDirective: ( + directive: astro.ClientDirectiveConfig + ) => void; + addDevOverlayPlugin: (entrypoint: string) => void; + addDevToolbarApp: ( + entrypoint: astro.DevToolbarAppEntry | string + ) => void; + addMiddleware: (mid: astro.AstroIntegrationMiddleware) => void; + logger: AstroIntegrationLogger; + }) => void; + }; + something: (it: string) => string; + } + >, + keyof astro.AstroIntegration + > + >; +``` + +Now the emitted declarations would be: + +```ts +import * as astro from "astro"; +import * as astro_integration_kit from "astro-integration-kit"; + +export const utility: astro_integration_kit.HookUtility< + "astro:config:setup", + [ + options: { + name: string; + } + ], + void +>; +export const integration: () => astro.AstroIntegration & { + something: (it: string) => string; +}; +``` diff --git a/package/src/core/define-integration.ts b/package/src/core/define-integration.ts index 718c0a37..7a74a646 100644 --- a/package/src/core/define-integration.ts +++ b/package/src/core/define-integration.ts @@ -2,13 +2,13 @@ import type { AstroIntegration } from "astro"; import { AstroError } from "astro/errors"; import { z } from "astro/zod"; import { errorMap } from "../internal/error-map.js"; -import type { Prettify } from "../internal/types.ts"; +import type { ExtendedPrettify } from "../internal/types.ts"; import type { Hooks } from "./types.js"; -type AstroIntegrationSetupFn = (params: { +type AstroIntegrationSetupFn = (params: { name: string; options: z.output; -}) => Omit & { +}) => Omit & TApi & { // Enable autocomplete and intellisense for non-core hooks hooks: Partial, }; @@ -34,9 +34,12 @@ type AstroIntegrationSetupFn = (params: { * ``` */ export const defineIntegration = < + TApiBase, + // Apply Prettify on a generic type parameter so it goes through + // the type expansion and beta reduction to form a minimal type + // for the emitted declarations on libraries. + TApi extends ExtendedPrettify>, TOptionsSchema extends z.ZodTypeAny = z.ZodNever, - TSetup extends - AstroIntegrationSetupFn = AstroIntegrationSetupFn, >({ name, optionsSchema, @@ -44,16 +47,15 @@ export const defineIntegration = < }: { name: string; optionsSchema?: TOptionsSchema; - setup: TSetup; + setup: AstroIntegrationSetupFn; }): (( ...args: [z.input] extends [never] ? [] : undefined extends z.input ? [options?: z.input] : [options: z.input] -) => AstroIntegration & - Prettify, keyof AstroIntegration>>) => { - return (...args): AstroIntegration & ReturnType => { +) => AstroIntegration & TApi) => { + return (...args): AstroIntegration & TApi => { const parsedOptions = (optionsSchema ?? z.never().optional()).safeParse( args[0], { @@ -70,11 +72,12 @@ export const defineIntegration = < const options = parsedOptions.data as z.output; - const integration = setup({ name, options }) as ReturnType; + const {hooks, ...extra} = setup({ name, options }); return { + ...extra as unknown as TApi, + hooks, name, - ...integration, }; }; }; diff --git a/package/src/core/define-utility.ts b/package/src/core/define-utility.ts index 5f28bc3f..4608a843 100644 --- a/package/src/core/define-utility.ts +++ b/package/src/core/define-utility.ts @@ -1,6 +1,17 @@ import type { HookParameters } from "astro"; import type { Hooks } from "./types.js"; +/** + * A utility to be used on an Astro hook. + * + * @see defineUtility + */ +export type HookUtility< + THook extends keyof Hooks, + TArgs extends Array, + TReturn, +> = (params: HookParameters, ...args: TArgs) => TReturn; + /** * Allows defining a type-safe function requiring all the params of a given hook. * It uses currying to make TypeScript happy. @@ -23,6 +34,6 @@ export const defineUtility = * @param {Function} fn; */ , T>( - fn: (params: HookParameters, ...args: TArgs) => T, - ) => + fn: HookUtility, + ): HookUtility => fn; diff --git a/package/src/core/index.ts b/package/src/core/index.ts index 87e8ce99..dc1306fd 100644 --- a/package/src/core/index.ts +++ b/package/src/core/index.ts @@ -2,7 +2,7 @@ export { createResolver } from "./create-resolver.js"; export { defineIntegration } from "./define-integration.js"; export { definePlugin } from "./define-plugin.js"; export { defineAllHooksPlugin } from "./define-all-hooks-plugin.js"; -export { defineUtility } from "./define-utility.js"; +export { defineUtility, type HookUtility } from "./define-utility.js"; export { withPlugins } from "./with-plugins.js"; export * from "./types.js"; export * from "../utilities/index.js"; diff --git a/package/src/internal/types.ts b/package/src/internal/types.ts index 8f785874..8fc7f5f2 100644 --- a/package/src/internal/types.ts +++ b/package/src/internal/types.ts @@ -28,4 +28,6 @@ export type Prettify = { [K in keyof T]: T[K]; } & {}; +export type ExtendedPrettify = T extends infer U ? Prettify : never; + export type NonEmptyArray = [T, ...Array];