Skip to content

Commit

Permalink
feat: Optimize emitted inferred types for libraries
Browse files Browse the repository at this point in the history
  • Loading branch information
Fryuni committed Jul 22, 2024
1 parent 5fa1688 commit 227f1dc
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 14 deletions.
127 changes: 127 additions & 0 deletions .changeset/flat-bugs-relate.md
Original file line number Diff line number Diff line change
@@ -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<T> = {
[K in keyof T]: T[K];
} & {};

type DeepPartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
? DeepPartial<U>[]
: T[P] extends object | undefined
? DeepPartial<T[P]>
: T[P];
};

export const utility: (
params: {
config: astro.AstroConfig;
command: "dev" | "build" | "preview";
isRestart: boolean;
updateConfig: (
newConfig: DeepPartial<astro.AstroConfig>
) => 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>
) => 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;
};
```
25 changes: 14 additions & 11 deletions package/src/core/define-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Options extends z.ZodTypeAny> = (params: {
type AstroIntegrationSetupFn<Options extends z.ZodTypeAny, TApi> = (params: {
name: string;
options: z.output<Options>;
}) => Omit<AstroIntegration, "name" | "hooks"> & {
}) => Omit<AstroIntegration, "name" | "hooks"> & TApi & {
// Enable autocomplete and intellisense for non-core hooks
hooks: Partial<Hooks>,
};
Expand All @@ -34,26 +34,28 @@ type AstroIntegrationSetupFn<Options extends z.ZodTypeAny> = (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<Omit<TApiBase, keyof AstroIntegration>>,
TOptionsSchema extends z.ZodTypeAny = z.ZodNever,
TSetup extends
AstroIntegrationSetupFn<TOptionsSchema> = AstroIntegrationSetupFn<TOptionsSchema>,
>({
name,
optionsSchema,
setup,
}: {
name: string;
optionsSchema?: TOptionsSchema;
setup: TSetup;
setup: AstroIntegrationSetupFn<TOptionsSchema, TApiBase>;
}): ((
...args: [z.input<TOptionsSchema>] extends [never]
? []
: undefined extends z.input<TOptionsSchema>
? [options?: z.input<TOptionsSchema>]
: [options: z.input<TOptionsSchema>]
) => AstroIntegration &
Prettify<Omit<ReturnType<TSetup>, keyof AstroIntegration>>) => {
return (...args): AstroIntegration & ReturnType<TSetup> => {
) => AstroIntegration & TApi) => {
return (...args): AstroIntegration & TApi => {
const parsedOptions = (optionsSchema ?? z.never().optional()).safeParse(
args[0],
{
Expand All @@ -70,11 +72,12 @@ export const defineIntegration = <

const options = parsedOptions.data as z.output<TOptionsSchema>;

const integration = setup({ name, options }) as ReturnType<TSetup>;
const {hooks, ...extra} = setup({ name, options });

return {
...extra as unknown as TApi,
hooks,
name,
...integration,
};
};
};
15 changes: 13 additions & 2 deletions package/src/core/define-utility.ts
Original file line number Diff line number Diff line change
@@ -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<any>,
TReturn,
> = (params: HookParameters<THook>, ...args: TArgs) => TReturn;

/**
* Allows defining a type-safe function requiring all the params of a given hook.
* It uses currying to make TypeScript happy.
Expand All @@ -23,6 +34,6 @@ export const defineUtility =
* @param {Function} fn;
*/
<TArgs extends Array<any>, T>(
fn: (params: HookParameters<THook>, ...args: TArgs) => T,
) =>
fn: HookUtility<THook, TArgs, T>,
): HookUtility<THook, TArgs, T> =>
fn;
2 changes: 1 addition & 1 deletion package/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
2 changes: 2 additions & 0 deletions package/src/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};

export type ExtendedPrettify<T> = T extends infer U ? Prettify<U> : never;

export type NonEmptyArray<T> = [T, ...Array<T>];

0 comments on commit 227f1dc

Please sign in to comment.