diff --git a/.changeset/green-deers-trade.md b/.changeset/green-deers-trade.md new file mode 100644 index 000000000000..065e7e520a51 --- /dev/null +++ b/.changeset/green-deers-trade.md @@ -0,0 +1,57 @@ +--- +"astro": minor +"@astrojs/db": minor +--- + +Adds a new `defineIntegration` helper and reworks typings for integrations authors extending Astro DB. + +This release adds a new `defineIntegration` helper through the `astro/integration` import that allows to define a type-safe integration and handle options validation (not required): + +```ts +import { defineIntegration } from "astro/integration" +import { z } from "astro/zod" + +export const integration = defineIntegration({ + name: "my-integration", + // optional + optionsSchema: z.object({ id: z.string() }), + setup({ options }) { + return { + hooks: { + "astro:config:setup": (params) => { + // ... + } + } + } + } +}) +``` + +Astro DB `defineDbIntegration` has been removed in favor of a way that works with this new `defineIntegration` (but also the `AstroIntegration` type): + +```ts +import {} from "astro" +import { defineIntegration } from "astro/integration" +import type { AstroDbHooks } from "@astrojs/db/types" + +declare module "astro" { + interface AstroIntegrationHooks extends AstroDbHooks {} +} + +export default defineIntegration({ + name: "db-test-integration", + setup() { + return { + hooks: { + 'astro:db:setup': ({ extendDb }) => { + extendDb({ + configEntrypoint: './integration/config.ts', + seedEntrypoint: './integration/seed.ts', + }); + }, + }, + } + } +}) + +``` \ No newline at end of file diff --git a/packages/astro/package.json b/packages/astro/package.json index 47528cdc5c50..7dad97ca3e97 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -53,6 +53,7 @@ "./client/*": "./dist/runtime/client/*", "./components": "./components/index.ts", "./components/*": "./components/*", + "./integration": "./dist/integrations/index.js", "./toolbar": "./dist/toolbar/index.js", "./assets": "./dist/assets/index.js", "./assets/utils": "./dist/assets/utils/index.js", @@ -69,6 +70,7 @@ "default": "./zod.mjs" }, "./errors": "./dist/core/errors/userError.js", + "./errors/zod-error-map": "./dist/core/errors/zod-error-map.js", "./middleware": { "types": "./dist/core/middleware/index.d.ts", "default": "./dist/core/middleware/index.js" diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 0cff203cf227..ffad72de4697 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1966,7 +1966,7 @@ export interface ResolvedInjectedRoute extends InjectedRoute { * Resolved Astro Config * Config with user settings along with all defaults filled in. */ -export interface AstroConfig extends AstroConfigType { +export interface AstroConfig extends Omit { // Public: // This is a more detailed type than zod validation gives us. // TypeScript still confirms zod validation matches this type. @@ -2716,87 +2716,89 @@ export interface SSRLoadedRenderer extends AstroRenderer { } export type HookParameters< - Hook extends keyof AstroIntegration['hooks'], - Fn = AstroIntegration['hooks'][Hook], + Hook extends keyof AstroIntegrationHooks, + Fn = AstroIntegrationHooks[Hook], > = Fn extends (...args: any) => any ? Parameters[0] : never; +export interface AstroIntegrationHooks { + 'astro:config:setup'?: (options: { + config: AstroConfig; + command: 'dev' | 'build' | 'preview'; + isRestart: boolean; + updateConfig: (newConfig: DeepPartial) => AstroConfig; + addRenderer: (renderer: AstroRenderer) => void; + addWatchFile: (path: URL | string) => void; + injectScript: (stage: InjectedScriptStage, content: string) => void; + injectRoute: (injectRoute: InjectedRoute) => void; + addClientDirective: (directive: ClientDirectiveConfig) => void; + /** + * @deprecated Use `addDevToolbarApp` instead. + * TODO: Fully remove in Astro 5.0 + */ + addDevOverlayPlugin: (entrypoint: string) => void; + // TODO: Deprecate the `string` overload once a few apps have been migrated to the new API. + addDevToolbarApp: (entrypoint: DevToolbarAppEntry | string) => void; + addMiddleware: (mid: AstroIntegrationMiddleware) => void; + logger: AstroIntegrationLogger; + // TODO: Add support for `injectElement()` for full HTML element injection, not just scripts. + // This may require some refactoring of `scripts`, `styles`, and `links` into something + // more generalized. Consider the SSR use-case as well. + // injectElement: (stage: vite.HtmlTagDescriptor, element: string) => void; + }) => void | Promise; + 'astro:config:done'?: (options: { + config: AstroConfig; + setAdapter: (adapter: AstroAdapter) => void; + logger: AstroIntegrationLogger; + }) => void | Promise; + 'astro:server:setup'?: (options: { + server: vite.ViteDevServer; + logger: AstroIntegrationLogger; + toolbar: ReturnType; + }) => void | Promise; + 'astro:server:start'?: (options: { + address: AddressInfo; + logger: AstroIntegrationLogger; + }) => void | Promise; + 'astro:server:done'?: (options: { logger: AstroIntegrationLogger }) => void | Promise; + 'astro:build:ssr'?: (options: { + manifest: SerializedSSRManifest; + /** + * This maps a {@link RouteData} to an {@link URL}, this URL represents + * the physical file you should import. + */ + entryPoints: Map; + /** + * File path of the emitted middleware + */ + middlewareEntryPoint: URL | undefined; + logger: AstroIntegrationLogger; + }) => void | Promise; + 'astro:build:start'?: (options: { logger: AstroIntegrationLogger }) => void | Promise; + 'astro:build:setup'?: (options: { + vite: vite.InlineConfig; + pages: Map; + target: 'client' | 'server'; + updateConfig: (newConfig: vite.InlineConfig) => void; + logger: AstroIntegrationLogger; + }) => void | Promise; + 'astro:build:generated'?: (options: { + dir: URL; + logger: AstroIntegrationLogger; + }) => void | Promise; + 'astro:build:done'?: (options: { + pages: { pathname: string }[]; + dir: URL; + routes: RouteData[]; + logger: AstroIntegrationLogger; + cacheManifest: boolean; + }) => void | Promise; +} + export interface AstroIntegration { /** The name of the integration. */ name: string; /** The different hooks available to extend. */ - hooks: { - 'astro:config:setup'?: (options: { - config: AstroConfig; - command: 'dev' | 'build' | 'preview'; - isRestart: boolean; - updateConfig: (newConfig: DeepPartial) => AstroConfig; - addRenderer: (renderer: AstroRenderer) => void; - addWatchFile: (path: URL | string) => void; - injectScript: (stage: InjectedScriptStage, content: string) => void; - injectRoute: (injectRoute: InjectedRoute) => void; - addClientDirective: (directive: ClientDirectiveConfig) => void; - /** - * @deprecated Use `addDevToolbarApp` instead. - * TODO: Fully remove in Astro 5.0 - */ - addDevOverlayPlugin: (entrypoint: string) => void; - // TODO: Deprecate the `string` overload once a few apps have been migrated to the new API. - addDevToolbarApp: (entrypoint: DevToolbarAppEntry | string) => void; - addMiddleware: (mid: AstroIntegrationMiddleware) => void; - logger: AstroIntegrationLogger; - // TODO: Add support for `injectElement()` for full HTML element injection, not just scripts. - // This may require some refactoring of `scripts`, `styles`, and `links` into something - // more generalized. Consider the SSR use-case as well. - // injectElement: (stage: vite.HtmlTagDescriptor, element: string) => void; - }) => void | Promise; - 'astro:config:done'?: (options: { - config: AstroConfig; - setAdapter: (adapter: AstroAdapter) => void; - logger: AstroIntegrationLogger; - }) => void | Promise; - 'astro:server:setup'?: (options: { - server: vite.ViteDevServer; - logger: AstroIntegrationLogger; - toolbar: ReturnType; - }) => void | Promise; - 'astro:server:start'?: (options: { - address: AddressInfo; - logger: AstroIntegrationLogger; - }) => void | Promise; - 'astro:server:done'?: (options: { logger: AstroIntegrationLogger }) => void | Promise; - 'astro:build:ssr'?: (options: { - manifest: SerializedSSRManifest; - /** - * This maps a {@link RouteData} to an {@link URL}, this URL represents - * the physical file you should import. - */ - entryPoints: Map; - /** - * File path of the emitted middleware - */ - middlewareEntryPoint: URL | undefined; - logger: AstroIntegrationLogger; - }) => void | Promise; - 'astro:build:start'?: (options: { logger: AstroIntegrationLogger }) => void | Promise; - 'astro:build:setup'?: (options: { - vite: vite.InlineConfig; - pages: Map; - target: 'client' | 'server'; - updateConfig: (newConfig: vite.InlineConfig) => void; - logger: AstroIntegrationLogger; - }) => void | Promise; - 'astro:build:generated'?: (options: { - dir: URL; - logger: AstroIntegrationLogger; - }) => void | Promise; - 'astro:build:done'?: (options: { - pages: { pathname: string }[]; - dir: URL; - routes: RouteData[]; - logger: AstroIntegrationLogger; - cacheManifest: boolean; - }) => void | Promise; - }; + hooks: AstroIntegrationHooks; } export type MiddlewareNext = () => Promise; diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index e2593e6f1288..3c696fabca75 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1156,6 +1156,18 @@ export const i18nNotEnabled = { hint: 'See https://docs.astro.build/en/guides/internationalization for a guide on setting up i18n.', } satisfies ErrorData; +/** + * @docs + * @description + * Invalid options have been passed to the integration. + */ +export const AstroIntegrationInvalidOptions = { + name: 'AstroIntegrationInvalidOptions', + title: 'Astro Integration Invalid Options', + message: (name: string, error: string) => + `Invalid options passed to "${name}" integration\n${error}`, +} satisfies ErrorData; + /** * @docs * @kind heading diff --git a/packages/astro/src/core/errors/index.ts b/packages/astro/src/core/errors/index.ts index 5a871ca29088..1889a72c67bd 100644 --- a/packages/astro/src/core/errors/index.ts +++ b/packages/astro/src/core/errors/index.ts @@ -11,4 +11,4 @@ export { export type { ErrorLocation, ErrorWithMetadata } from './errors.js'; export { codeFrame } from './printer.js'; export { createSafeError, positionAt } from './utils.js'; -export { errorMap } from './zod-error-map.js'; \ No newline at end of file +export { errorMap } from './zod-error-map.js'; diff --git a/packages/astro/src/core/errors/zod-error-map.ts b/packages/astro/src/core/errors/zod-error-map.ts index c3372b708b5c..615b7bca0b65 100644 --- a/packages/astro/src/core/errors/zod-error-map.ts +++ b/packages/astro/src/core/errors/zod-error-map.ts @@ -1,6 +1,6 @@ import type { ZodErrorMap } from 'zod'; -type TypeOrLiteralErrByPathEntry = { +interface TypeOrLiteralErrByPathEntry { code: 'invalid_type' | 'invalid_literal'; received: unknown; expected: unknown[]; @@ -14,12 +14,13 @@ export const errorMap: ZodErrorMap = (baseError, ctx) => { // raise a single error when `key` does not match: // > Did not match union. // > key: Expected `'tutorial' | 'blog'`, received 'foo' - let typeOrLiteralErrByPath = new Map(); - for (const unionError of baseError.unionErrors.map((e) => e.errors).flat()) { + const typeOrLiteralErrByPath = new Map(); + for (const unionError of baseError.unionErrors.flatMap((e) => e.errors)) { if (unionError.code === 'invalid_type' || unionError.code === 'invalid_literal') { const flattenedErrorPath = flattenErrorPath(unionError.path); - if (typeOrLiteralErrByPath.has(flattenedErrorPath)) { - typeOrLiteralErrByPath.get(flattenedErrorPath)!.expected.push(unionError.expected); + const typeOrLiteralErr = typeOrLiteralErrByPath.get(flattenedErrorPath); + if (typeOrLiteralErr) { + typeOrLiteralErr.expected.push(unionError.expected); } else { typeOrLiteralErrByPath.set(flattenedErrorPath, { code: unionError.code, @@ -29,7 +30,7 @@ export const errorMap: ZodErrorMap = (baseError, ctx) => { } } } - let messages: string[] = [ + const messages: string[] = [ prefix( baseErrorPath, typeOrLiteralErrByPath.size ? 'Did not match union:' : 'Did not match union.' @@ -43,9 +44,9 @@ export const errorMap: ZodErrorMap = (baseError, ctx) => { // filter it out. Can lead to confusing noise. .filter(([, error]) => error.expected.length === baseError.unionErrors.length) .map(([key, error]) => + // Avoid printing the key again if it's a base error key === baseErrorPath - ? // Avoid printing the key again if it's a base error - `> ${getTypeOrLiteralMsg(error)}` + ? `> ${getTypeOrLiteralMsg(error)}` : `> ${prefix(key, getTypeOrLiteralMsg(error))}` ) ) @@ -96,4 +97,4 @@ const unionExpectedVals = (expectedVals: Set) => }) .join(''); -const flattenErrorPath = (errorPath: (string | number)[]) => errorPath.join('.'); +const flattenErrorPath = (errorPath: Array) => errorPath.join('.'); diff --git a/packages/astro/src/integrations/define-integration.ts b/packages/astro/src/integrations/define-integration.ts new file mode 100644 index 000000000000..3e44db05e868 --- /dev/null +++ b/packages/astro/src/integrations/define-integration.ts @@ -0,0 +1,60 @@ +import type { AstroIntegration, AstroIntegrationHooks } from '../@types/astro.js'; +import { AstroError, AstroErrorData, errorMap } from '../core/errors/index.js'; +import { z } from 'zod'; + +type AstroIntegrationSetupFn = (params: { + name: string; + options: z.output; +}) => { + hooks: AstroIntegrationHooks; +}; + +/** + * Allows defining an integration in a type-safe way and optionally validate options. + * See [documentation](TODO:). +*/ +export const defineIntegration = < + TOptionsSchema extends z.ZodTypeAny = z.ZodNever, + TSetup extends AstroIntegrationSetupFn = AstroIntegrationSetupFn, +>({ + name, + optionsSchema, + setup, +}: { + name: string; + optionsSchema?: TOptionsSchema; + setup: TSetup; +}): (( + ...args: [z.input] extends [never] + ? [] + : undefined extends z.input + ? [options?: z.input] + : [options: z.input] +) => AstroIntegration & Omit, keyof AstroIntegration>) => { + return (...args) => { + const parsedOptions = (optionsSchema ?? z.never().optional()).safeParse(args[0], { + errorMap, + }); + + if (!parsedOptions.success) { + throw new AstroError({ + ...AstroErrorData.AstroIntegrationInvalidOptions, + message: AstroErrorData.AstroIntegrationInvalidOptions.message( + name, + parsedOptions.error.issues.map((i) => i.message).join('\n') + ), + }); + } + + const options = parsedOptions.data as z.output; + + const integration = setup({ name, options }) as ReturnType; + + return { + name, + ...integration, + }; + }; +}; + +// export const defineIntegration = () => {}; diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts new file mode 100644 index 000000000000..8628ca745516 --- /dev/null +++ b/packages/astro/src/integrations/index.ts @@ -0,0 +1 @@ +export { defineIntegration } from "./define-integration.js" \ No newline at end of file diff --git a/packages/astro/test/units/integrations/define-integration.test.js b/packages/astro/test/units/integrations/define-integration.test.js new file mode 100644 index 000000000000..b1a6db68926e --- /dev/null +++ b/packages/astro/test/units/integrations/define-integration.test.js @@ -0,0 +1,146 @@ +import { describe, it, beforeEach } from 'node:test'; +import assert from 'node:assert/strict'; +import { defineIntegration } from '../../../dist/integrations/define-integration.js'; +import { z } from '../../../zod.mjs'; + +/** + * @typedef {Parameters[0]} DefineIntegrationParams + */ + +const createFixture = () => { + let name = 'my-integration'; + /** @type {DefineIntegrationParams["setup"]} */ + let setup = () => ({ + hooks: {}, + }); + /** @type {DefineIntegrationParams["optionsSchema"]} */ + let optionsSchema; + + return { + /** @param {typeof name} _name */ + givenName(_name) { + name = _name; + }, + /** @param {typeof setup} _setup */ + givenSetup(_setup) { + setup = _setup; + }, + /** @param {typeof optionsSchema} _optionsSchema */ + givenOptionsSchema(_optionsSchema) { + optionsSchema = _optionsSchema; + }, + /** @param {import("astro").AstroIntegration} expected */ + thenIntegrationShouldBe(expected) { + const resolved = defineIntegration({ + name, + optionsSchema, + setup, + })(); + assert.equal(expected.name, resolved.name); + assert.deepEqual(Object.keys(expected.hooks).sort(), Object.keys(resolved.hooks).sort()); + }, + /** + * @param {string} key + * @param {any} value + */ + thenExtraFieldShouldBe(key, value) { + const resolved = defineIntegration({ + name, + optionsSchema, + setup, + })(); + assert.equal(true, Object.keys(resolved).includes(key)); + assert.deepEqual(value, resolved[key]); + }, + /** @param {any} options */ + thenIntegrationCreationShouldNotThrow(options) { + assert.doesNotThrow(() => + defineIntegration({ + name, + optionsSchema, + setup, + })(options) + ); + }, + /** @param {any} options */ + thenIntegrationCreationShouldThrow(options) { + assert.throws(() => + defineIntegration({ + name, + optionsSchema, + setup, + })(options) + ); + }, + }; +}; + +describe('core: defineIntegration', () => { + /** @type {ReturnType} */ + let fixture; + + beforeEach(() => { + fixture = createFixture(); + }); + + it('Should return the correct integration with no hooks', () => { + const name = 'my-integration'; + const setup = () => ({ hooks: {} }); + + fixture.givenName(name); + fixture.givenSetup(setup); + + fixture.thenIntegrationShouldBe({ + name: 'my-integration', + hooks: {}, + }); + }); + + it('Should return the correct integration with some hooks', () => { + const name = 'my-integration'; + const setup = () => ({ + hooks: { + 'astro:config:setup': () => {}, + 'astro:server:start': () => {}, + }, + }); + + fixture.givenName(name); + fixture.givenSetup(setup); + + fixture.thenIntegrationShouldBe({ + name: 'my-integration', + hooks: { + 'astro:server:start': () => {}, + 'astro:config:setup': () => {}, + }, + }); + }); + + it('Should handle optionsSchema correctly', () => { + const optionsSchema = z.object({ + foo: z.string(), + }); + + fixture.givenOptionsSchema(optionsSchema); + fixture.thenIntegrationCreationShouldNotThrow({ + foo: 'bar', + }); + fixture.thenIntegrationCreationShouldThrow(null); + fixture.thenIntegrationCreationShouldThrow({ + foo: 123, + }); + }); + + it('Should accept any extra field from setup', () => { + const setup = () => ({ + hooks: {}, + config: { + foo: 'bar', + }, + }); + + fixture.givenSetup(setup); + fixture.thenExtraFieldShouldBe('config', { foo: 'bar' }); + }); +}); diff --git a/packages/db/src/core/integration/error-map.ts b/packages/db/src/core/integration/error-map.ts deleted file mode 100644 index e471fead5df1..000000000000 --- a/packages/db/src/core/integration/error-map.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * This is a modified version of Astro's error map. source: - * https://github.com/withastro/astro/blob/main/packages/astro/src/content/error-map.ts - */ -import type { z } from 'astro/zod'; - -interface TypeOrLiteralErrByPathEntry { - code: 'invalid_type' | 'invalid_literal'; - received: unknown; - expected: unknown[]; -} - -export const errorMap: z.ZodErrorMap = (baseError, ctx) => { - const baseErrorPath = flattenErrorPath(baseError.path); - if (baseError.code === 'invalid_union') { - // Optimization: Combine type and literal errors for keys that are common across ALL union types - // Ex. a union between `{ key: z.literal('tutorial') }` and `{ key: z.literal('blog') }` will - // raise a single error when `key` does not match: - // > Did not match union. - // > key: Expected `'tutorial' | 'blog'`, received 'foo' - const typeOrLiteralErrByPath = new Map(); - for (const unionError of baseError.unionErrors.flatMap((e) => e.errors)) { - if (unionError.code === 'invalid_type' || unionError.code === 'invalid_literal') { - const flattenedErrorPath = flattenErrorPath(unionError.path); - const typeOrLiteralErr = typeOrLiteralErrByPath.get(flattenedErrorPath); - if (typeOrLiteralErr) { - typeOrLiteralErr.expected.push(unionError.expected); - } else { - typeOrLiteralErrByPath.set(flattenedErrorPath, { - code: unionError.code, - received: (unionError as any).received, - expected: [unionError.expected], - }); - } - } - } - const messages: string[] = [ - prefix( - baseErrorPath, - typeOrLiteralErrByPath.size ? 'Did not match union:' : 'Did not match union.' - ), - ]; - return { - message: messages - .concat( - [...typeOrLiteralErrByPath.entries()] - // If type or literal error isn't common to ALL union types, - // filter it out. Can lead to confusing noise. - .filter(([, error]) => error.expected.length === baseError.unionErrors.length) - .map(([key, error]) => - // Avoid printing the key again if it's a base error - key === baseErrorPath - ? `> ${getTypeOrLiteralMsg(error)}` - : `> ${prefix(key, getTypeOrLiteralMsg(error))}` - ) - ) - .join('\n'), - }; - } - if (baseError.code === 'invalid_literal' || baseError.code === 'invalid_type') { - return { - message: prefix( - baseErrorPath, - getTypeOrLiteralMsg({ - code: baseError.code, - received: (baseError as any).received, - expected: [baseError.expected], - }) - ), - }; - } else if (baseError.message) { - return { message: prefix(baseErrorPath, baseError.message) }; - } else { - return { message: prefix(baseErrorPath, ctx.defaultError) }; - } -}; - -const getTypeOrLiteralMsg = (error: TypeOrLiteralErrByPathEntry): string => { - if (error.received === 'undefined') return 'Required'; - const expectedDeduped = new Set(error.expected); - switch (error.code) { - case 'invalid_type': - return `Expected type \`${unionExpectedVals(expectedDeduped)}\`, received ${JSON.stringify( - error.received - )}`; - case 'invalid_literal': - return `Expected \`${unionExpectedVals(expectedDeduped)}\`, received ${JSON.stringify( - error.received - )}`; - } -}; - -const prefix = (key: string, msg: string) => (key.length ? `**${key}**: ${msg}` : msg); - -const unionExpectedVals = (expectedVals: Set) => - [...expectedVals] - .map((expectedVal, idx) => { - if (idx === 0) return JSON.stringify(expectedVal); - const sep = ' | '; - return `${sep}${JSON.stringify(expectedVal)}`; - }) - .join(''); - -const flattenErrorPath = (errorPath: Array) => errorPath.join('.'); diff --git a/packages/db/src/core/load-file.ts b/packages/db/src/core/load-file.ts index 7bc7387c824c..35fd7e953a31 100644 --- a/packages/db/src/core/load-file.ts +++ b/packages/db/src/core/load-file.ts @@ -6,13 +6,13 @@ import type { AstroConfig, AstroIntegration } from 'astro'; import { build as esbuild } from 'esbuild'; import { CONFIG_FILE_NAMES, VIRTUAL_MODULE_ID } from './consts.js'; import { INTEGRATION_TABLE_CONFLICT_ERROR } from './errors.js'; -import { errorMap } from './integration/error-map.js'; +import { errorMap } from 'astro/errors/zod-error-map'; import { getConfigVirtualModContents } from './integration/vite-plugin-db.js'; import { dbConfigSchema } from './schemas.js'; -import { type AstroDbIntegration } from './types.js'; +import { type AstroDbHooks } from './types.js'; import { getDbDirectoryUrl } from './utils.js'; -const isDbIntegration = (integration: AstroIntegration): integration is AstroDbIntegration => +const isDbIntegration = (integration: AstroIntegration): integration is AstroIntegration & { hooks: AstroDbHooks } => 'astro:db:setup' in integration.hooks; /** diff --git a/packages/db/src/core/schemas.ts b/packages/db/src/core/schemas.ts index a2a8368fbfb6..7ab906b410ca 100644 --- a/packages/db/src/core/schemas.ts +++ b/packages/db/src/core/schemas.ts @@ -2,7 +2,7 @@ import { SQL } from 'drizzle-orm'; import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; import { type ZodTypeDef, z } from 'zod'; import { SERIALIZED_SQL_KEY, type SerializedSQL } from '../runtime/types.js'; -import { errorMap } from './integration/error-map.js'; +import { errorMap } from 'astro/errors/zod-error-map'; import type { NumberColumn, TextColumn } from './types.js'; import { mapObject } from './utils.js'; diff --git a/packages/db/src/core/types.ts b/packages/db/src/core/types.ts index 79bbdf371927..cbf7bdd894ee 100644 --- a/packages/db/src/core/types.ts +++ b/packages/db/src/core/types.ts @@ -88,13 +88,11 @@ interface LegacyIndexConfig export type NumberColumnOpts = z.input; export type TextColumnOpts = z.input; -export type AstroDbIntegration = AstroIntegration & { - hooks: { - 'astro:db:setup'?: (options: { - extendDb: (options: { - configEntrypoint?: URL | string; - seedEntrypoint?: URL | string; - }) => void; - }) => void | Promise; - }; -}; +export interface AstroDbHooks { + 'astro:db:setup'?: (options: { + extendDb: (options: { + configEntrypoint?: URL | string; + seedEntrypoint?: URL | string; + }) => void; + }) => void | Promise; +} diff --git a/packages/db/src/core/utils.ts b/packages/db/src/core/utils.ts index ebc2547b3e2e..65569c4924b2 100644 --- a/packages/db/src/core/utils.ts +++ b/packages/db/src/core/utils.ts @@ -1,6 +1,5 @@ -import type { AstroConfig, AstroIntegration } from 'astro'; +import type { AstroConfig } from 'astro'; import { loadEnv } from 'vite'; -import type { AstroDbIntegration } from './types.js'; export type VitePlugin = Required['plugins'][number]; @@ -23,10 +22,6 @@ export function getDbDirectoryUrl(root: URL | string) { return new URL('db/', root); } -export function defineDbIntegration(integration: AstroDbIntegration): AstroIntegration { - return integration; -} - export type Result = { success: true; data: T } | { success: false; data: unknown }; /** diff --git a/packages/db/src/utils.ts b/packages/db/src/utils.ts index 0f244cd00be5..54e3b05674c8 100644 --- a/packages/db/src/utils.ts +++ b/packages/db/src/utils.ts @@ -1,4 +1,3 @@ -export { defineDbIntegration } from './core/utils.js'; import { tableSchema } from './core/schemas.js'; import type { ColumnsConfig, TableConfig } from './core/types.js'; import { type Table, asDrizzleTable as internal_asDrizzleTable } from './runtime/index.js'; diff --git a/packages/db/test/fixtures/integration-only/integration/index.ts b/packages/db/test/fixtures/integration-only/integration/index.ts index b249cc253853..431a1e935afb 100644 --- a/packages/db/test/fixtures/integration-only/integration/index.ts +++ b/packages/db/test/fixtures/integration-only/integration/index.ts @@ -1,15 +1,23 @@ -import { defineDbIntegration } from '@astrojs/db/utils'; +import {} from "astro" +import { defineIntegration } from "astro/integration" +import type { AstroDbHooks } from "@astrojs/db/types" -export default function testIntegration() { - return defineDbIntegration({ - name: 'db-test-integration', - hooks: { - 'astro:db:setup'({ extendDb }) { +declare module "astro" { + interface AstroIntegrationHooks extends AstroDbHooks {} +} + +export default defineIntegration({ + name: "db-test-integration", + setup() { + return { + hooks: { + 'astro:db:setup': ({ extendDb }) => { extendDb({ configEntrypoint: './integration/config.ts', seedEntrypoint: './integration/seed.ts', }); }, }, - }); -} + } + } +}) diff --git a/packages/db/test/fixtures/integrations/integration/index.ts b/packages/db/test/fixtures/integrations/integration/index.ts index b249cc253853..431a1e935afb 100644 --- a/packages/db/test/fixtures/integrations/integration/index.ts +++ b/packages/db/test/fixtures/integrations/integration/index.ts @@ -1,15 +1,23 @@ -import { defineDbIntegration } from '@astrojs/db/utils'; +import {} from "astro" +import { defineIntegration } from "astro/integration" +import type { AstroDbHooks } from "@astrojs/db/types" -export default function testIntegration() { - return defineDbIntegration({ - name: 'db-test-integration', - hooks: { - 'astro:db:setup'({ extendDb }) { +declare module "astro" { + interface AstroIntegrationHooks extends AstroDbHooks {} +} + +export default defineIntegration({ + name: "db-test-integration", + setup() { + return { + hooks: { + 'astro:db:setup': ({ extendDb }) => { extendDb({ configEntrypoint: './integration/config.ts', seedEntrypoint: './integration/seed.ts', }); }, }, - }); -} + } + } +})