diff --git a/.changeset/fresh-fans-study.md b/.changeset/fresh-fans-study.md new file mode 100644 index 000000000000..9a837b1fd7a7 --- /dev/null +++ b/.changeset/fresh-fans-study.md @@ -0,0 +1,18 @@ +--- +'@astrojs/db': minor +--- + +Changes how type generation works + +The generated `.d.ts` file is now at a new location: + +```diff +- .astro/db-types.d.ts ++ .astro/integrations/astro_db/db.d.ts +``` + +The following line can now be removed from `src/env.d.ts`: + +```diff +- /// +``` diff --git a/.changeset/mean-horses-kiss.md b/.changeset/mean-horses-kiss.md new file mode 100644 index 000000000000..7d211e62674f --- /dev/null +++ b/.changeset/mean-horses-kiss.md @@ -0,0 +1,35 @@ +--- +'astro': minor +--- + +Adds a new [`injectTypes()` utility](https://docs.astro.build/en/reference/integrations-reference/#injecttypes-options) to the Integration API and refactors how type generation works + +Use `injectTypes()` in the `astro:config:done` hook to inject types into your user's project by adding a new a `*.d.ts` file. + +The `filename` property will be used to generate a file at `/.astro/integrations//.d.ts` and must end with `".d.ts"`. + +The `content` property will create the body of the file, and must be valid TypeScript. + +Additionally, `injectTypes()` returns a URL to the normalized path so you can overwrite its content later on, or manipulate it in any way you want. + +```js +// my-integration/index.js +export default { + name: 'my-integration', + 'astro:config:done': ({ injectTypes }) => { + injectTypes({ + filename: "types.d.ts", + content: "declare module 'virtual:my-integration' {}" + }) + } +}; +``` + +Codegen has been refactored. Although `src/env.d.ts` will continue to work as is, we recommend you update it: + +```diff +- /// ++ /// +- /// +- /// +``` \ No newline at end of file diff --git a/examples/basics/src/env.d.ts b/examples/basics/src/env.d.ts index f964fe0cffd8..9bc5cb41c24e 100644 --- a/examples/basics/src/env.d.ts +++ b/examples/basics/src/env.d.ts @@ -1 +1 @@ -/// +/// \ No newline at end of file diff --git a/examples/blog/src/env.d.ts b/examples/blog/src/env.d.ts index acef35f175aa..e16c13c6952a 100644 --- a/examples/blog/src/env.d.ts +++ b/examples/blog/src/env.d.ts @@ -1,2 +1 @@ /// -/// diff --git a/examples/framework-alpine/src/env.d.ts b/examples/framework-alpine/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-alpine/src/env.d.ts +++ b/examples/framework-alpine/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/framework-lit/src/env.d.ts b/examples/framework-lit/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-lit/src/env.d.ts +++ b/examples/framework-lit/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/framework-multiple/src/env.d.ts b/examples/framework-multiple/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-multiple/src/env.d.ts +++ b/examples/framework-multiple/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/framework-preact/src/env.d.ts b/examples/framework-preact/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-preact/src/env.d.ts +++ b/examples/framework-preact/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/framework-react/src/env.d.ts b/examples/framework-react/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-react/src/env.d.ts +++ b/examples/framework-react/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/framework-solid/src/env.d.ts b/examples/framework-solid/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-solid/src/env.d.ts +++ b/examples/framework-solid/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/framework-svelte/src/env.d.ts b/examples/framework-svelte/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-svelte/src/env.d.ts +++ b/examples/framework-svelte/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/framework-vue/src/env.d.ts b/examples/framework-vue/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/framework-vue/src/env.d.ts +++ b/examples/framework-vue/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/hackernews/src/env.d.ts b/examples/hackernews/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/hackernews/src/env.d.ts +++ b/examples/hackernews/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/middleware/src/env.d.ts b/examples/middleware/src/env.d.ts index 44f67965a3cc..74b9019e5746 100644 --- a/examples/middleware/src/env.d.ts +++ b/examples/middleware/src/env.d.ts @@ -1,4 +1,4 @@ -/// +/// declare namespace App { interface Locals { user: { diff --git a/examples/minimal/src/env.d.ts b/examples/minimal/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/minimal/src/env.d.ts +++ b/examples/minimal/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/non-html-pages/src/env.d.ts b/examples/non-html-pages/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/non-html-pages/src/env.d.ts +++ b/examples/non-html-pages/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/portfolio/src/env.d.ts b/examples/portfolio/src/env.d.ts index acef35f175aa..e16c13c6952a 100644 --- a/examples/portfolio/src/env.d.ts +++ b/examples/portfolio/src/env.d.ts @@ -1,2 +1 @@ /// -/// diff --git a/examples/ssr/src/env.d.ts b/examples/ssr/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/ssr/src/env.d.ts +++ b/examples/ssr/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/with-markdoc/src/env.d.ts b/examples/with-markdoc/src/env.d.ts index acef35f175aa..e16c13c6952a 100644 --- a/examples/with-markdoc/src/env.d.ts +++ b/examples/with-markdoc/src/env.d.ts @@ -1,2 +1 @@ /// -/// diff --git a/examples/with-markdown-plugins/src/env.d.ts b/examples/with-markdown-plugins/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/with-markdown-plugins/src/env.d.ts +++ b/examples/with-markdown-plugins/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/with-markdown-shiki/src/env.d.ts b/examples/with-markdown-shiki/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/with-markdown-shiki/src/env.d.ts +++ b/examples/with-markdown-shiki/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/with-mdx/src/env.d.ts b/examples/with-mdx/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/with-mdx/src/env.d.ts +++ b/examples/with-mdx/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/with-nanostores/src/env.d.ts b/examples/with-nanostores/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/with-nanostores/src/env.d.ts +++ b/examples/with-nanostores/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/examples/with-tailwindcss/src/env.d.ts b/examples/with-tailwindcss/src/env.d.ts index f964fe0cffd8..e16c13c6952a 100644 --- a/examples/with-tailwindcss/src/env.d.ts +++ b/examples/with-tailwindcss/src/env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index ec7848a17ecc..129d52f638a6 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -2553,6 +2553,11 @@ export interface AstroAdapterFeatures { functionPerRoute: boolean; } +export interface InjectedType { + filename: string; + content: string; +} + export interface AstroSettings { config: AstroConfig; adapter: AstroAdapter | undefined; @@ -2588,6 +2593,7 @@ export interface AstroSettings { latestAstroVersion: string | undefined; serverIslandMap: NonNullable; serverIslandNameMap: NonNullable; + injectedTypes: Array; } export type AsyncRendererComponentFn = ( @@ -3289,6 +3295,7 @@ declare global { 'astro:config:done': (options: { config: AstroConfig; setAdapter: (adapter: AstroAdapter) => void; + injectTypes: (injectedType: InjectedType) => URL; logger: AstroIntegrationLogger; }) => void | Promise; 'astro:server:setup': (options: { diff --git a/packages/astro/src/actions/consts.ts b/packages/astro/src/actions/consts.ts index 6a55386d869a..beb8c45b641d 100644 --- a/packages/astro/src/actions/consts.ts +++ b/packages/astro/src/actions/consts.ts @@ -1,6 +1,6 @@ export const VIRTUAL_MODULE_ID = 'astro:actions'; export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID; -export const ACTIONS_TYPES_FILE = 'actions.d.ts'; +export const ACTIONS_TYPES_FILE = 'astro/actions.d.ts'; export const VIRTUAL_INTERNAL_MODULE_ID = 'astro:internal-actions'; export const RESOLVED_VIRTUAL_INTERNAL_MODULE_ID = '\0astro:internal-actions'; export const NOOP_ACTIONS = '\0noop-actions'; diff --git a/packages/astro/src/actions/index.ts b/packages/astro/src/actions/index.ts index 04a911856f9c..2423b7017d43 100644 --- a/packages/astro/src/actions/index.ts +++ b/packages/astro/src/actions/index.ts @@ -30,9 +30,6 @@ export default function astroActions({ throw error; } - const stringifiedActionsImport = JSON.stringify( - viteID(new URL('./actions', params.config.srcDir)), - ); params.updateConfig({ vite: { plugins: [vitePluginUserActions({ settings }), vitePluginActions(fs)], @@ -49,11 +46,18 @@ export default function astroActions({ entrypoint: 'astro/actions/runtime/middleware.js', order: 'post', }); + }, + 'astro:config:done': (params) => { + const stringifiedActionsImport = JSON.stringify( + viteID(new URL('./actions', params.config.srcDir)), + ); + settings.injectedTypes.push({ + filename: ACTIONS_TYPES_FILE, + content: `declare module "astro:actions" { + type Actions = typeof import(${stringifiedActionsImport})["server"]; - await typegen({ - stringifiedActionsImport, - root: params.config.root, - fs, + export const actions: Actions; +}`, }); }, }, @@ -119,24 +123,3 @@ const vitePluginActions = (fs: typeof fsMod): VitePlugin => ({ return code; }, }); - -async function typegen({ - stringifiedActionsImport, - root, - fs, -}: { - stringifiedActionsImport: string; - root: URL; - fs: typeof fsMod; -}) { - const content = `declare module "astro:actions" { - type Actions = typeof import(${stringifiedActionsImport})["server"]; - - export const actions: Actions; -}`; - - const dotAstroDir = new URL('.astro/', root); - - await fs.promises.mkdir(dotAstroDir, { recursive: true }); - await fs.promises.writeFile(new URL(ACTIONS_TYPES_FILE, dotAstroDir), content); -} diff --git a/packages/astro/src/cli/check/index.ts b/packages/astro/src/cli/check/index.ts index a95e1074a59a..ebb89e7356d6 100644 --- a/packages/astro/src/cli/check/index.ts +++ b/packages/astro/src/cli/check/index.ts @@ -30,7 +30,7 @@ export async function check(flags: Arguments) { // For now, we run this once as usually `astro check --watch` is ran alongside `astro dev` which also calls `astro sync`. const { default: sync } = await import('../../core/sync/index.js'); try { - await sync({ inlineConfig: flagsToAstroInlineConfig(flags) }); + await sync(flagsToAstroInlineConfig(flags)); } catch (_) { return process.exit(1); } diff --git a/packages/astro/src/cli/sync/index.ts b/packages/astro/src/cli/sync/index.ts index d10bbb7d286e..235b050a6b77 100644 --- a/packages/astro/src/cli/sync/index.ts +++ b/packages/astro/src/cli/sync/index.ts @@ -24,10 +24,7 @@ export async function sync({ flags }: SyncOptions) { } try { - await _sync({ - inlineConfig: flagsToAstroInlineConfig(flags), - telemetry: true, - }); + await _sync(flagsToAstroInlineConfig(flags), { telemetry: true }); return 0; } catch (_) { return 1; diff --git a/packages/astro/src/content/consts.ts b/packages/astro/src/content/consts.ts index b506e08ab03f..ac619c2b5e78 100644 --- a/packages/astro/src/content/consts.ts +++ b/packages/astro/src/content/consts.ts @@ -34,7 +34,7 @@ export const CONTENT_FLAGS = [ CONTENT_MODULE_FLAG, ] as const; -export const CONTENT_TYPES_FILE = 'types.d.ts'; +export const CONTENT_TYPES_FILE = 'astro/content.d.ts'; export const DATA_STORE_FILE = 'data-store.json'; export const ASSET_IMPORTS_FILE = 'assets.mjs'; diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index 8fa216fc1551..2394f43fa631 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -530,7 +530,7 @@ async function writeContentFiles({ } const configPathRelativeToCacheDir = normalizeConfigPath( - settings.dotAstroDir.pathname, + new URL('astro', settings.dotAstroDir).pathname, contentPaths.config.url.pathname, ); @@ -546,8 +546,17 @@ async function writeContentFiles({ contentConfig ? `typeof import(${configPathRelativeToCacheDir})` : 'never', ); - await fs.promises.writeFile( - new URL(CONTENT_TYPES_FILE, settings.dotAstroDir), - typeTemplateContent, - ); + // If it's the first time, we inject types the usual way. sync() will handle creating files and references. If it's not the first time, we just override the dts content + if (settings.injectedTypes.some((t) => t.filename === CONTENT_TYPES_FILE)) { + fs.promises.writeFile( + new URL(CONTENT_TYPES_FILE, settings.dotAstroDir), + typeTemplateContent, + 'utf-8', + ); + } else { + settings.injectedTypes.push({ + filename: CONTENT_TYPES_FILE, + content: typeTemplateContent, + }); + } } diff --git a/packages/astro/src/core/config/settings.ts b/packages/astro/src/core/config/settings.ts index c3c62edd45a0..6c878d7f3323 100644 --- a/packages/astro/src/core/config/settings.ts +++ b/packages/astro/src/core/config/settings.ts @@ -14,7 +14,8 @@ import { loadTSConfig } from './tsconfig.js'; export function createBaseSettings(config: AstroConfig): AstroSettings { const { contentDir } = getContentPaths(config); - const preferences = createPreferences(config); + const dotAstroDir = new URL('.astro/', config.root); + const preferences = createPreferences(config, dotAstroDir); return { config, preferences, @@ -106,8 +107,9 @@ export function createBaseSettings(config: AstroConfig): AstroSettings { watchFiles: [], devToolbarApps: [], timer: new AstroTimer(), - dotAstroDir: new URL('.astro/', config.root), + dotAstroDir, latestAstroVersion: undefined, // Will be set later if applicable when the dev server starts + injectedTypes: [], }; } diff --git a/packages/astro/src/core/dev/restart.ts b/packages/astro/src/core/dev/restart.ts index 4c970c645225..235454cb5a70 100644 --- a/packages/astro/src/core/dev/restart.ts +++ b/packages/astro/src/core/dev/restart.ts @@ -31,7 +31,6 @@ async function createRestartedContainer( } const configRE = /.*astro.config.(?:mjs|cjs|js|ts)$/; -const preferencesRE = /.*\.astro\/settings.json$/; function shouldRestartContainer( { settings, inlineConfig, restartInFlight }: Container, @@ -40,17 +39,19 @@ function shouldRestartContainer( if (restartInFlight) return false; let shouldRestart = false; + const normalizedChangedFile = vite.normalizePath(changedFile); // If the config file changed, reload the config and restart the server. if (inlineConfig.configFile) { - shouldRestart = vite.normalizePath(inlineConfig.configFile) === vite.normalizePath(changedFile); + shouldRestart = vite.normalizePath(inlineConfig.configFile) === normalizedChangedFile; } // Otherwise, watch for any astro.config.* file changes in project root else { - const normalizedChangedFile = vite.normalizePath(changedFile); shouldRestart = configRE.test(normalizedChangedFile); - - if (preferencesRE.test(normalizedChangedFile)) { + const settingsPath = vite.normalizePath( + fileURLToPath(new URL('settings.json', settings.dotAstroDir)), + ); + if (settingsPath.match(normalizedChangedFile)) { shouldRestart = settings.preferences.ignoreNextPreferenceReload ? false : true; settings.preferences.ignoreNextPreferenceReload = false; diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index d4ad380c0be5..21ab487f0931 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1291,6 +1291,17 @@ export const RewriteWithBodyUsed = { 'Astro.rewrite() cannot be used if the request body has already been read. If you need to read the body, first clone the request.', } satisfies ErrorData; +/** + * @docs + * @description + * An unknown error occured while reading or writing files to disk. It can be caused by many things, eg. missing permissions or a file not existing we attempt to read. + */ +export const UnknownFilesystemError = { + name: 'UnknownFilesystemError', + title: 'An unknown error occured while reading or writing files to disk.', + hint: 'It can be caused by many things, eg. missing permissions or a file not existing we attempt to read. Check the error cause for more details.', +} satisfies ErrorData; + /** * @docs * @kind heading diff --git a/packages/astro/src/core/index.ts b/packages/astro/src/core/index.ts index e0f9f6c82412..bdd7c7f05939 100644 --- a/packages/astro/src/core/index.ts +++ b/packages/astro/src/core/index.ts @@ -23,4 +23,4 @@ export const build = (inlineConfig: AstroInlineConfig) => _build(inlineConfig); * @experimental The JavaScript API is experimental */ // Wrap `_sync` to prevent exposing internal options -export const sync = (inlineConfig: AstroInlineConfig) => _sync({ inlineConfig }); +export const sync = (inlineConfig: AstroInlineConfig) => _sync(inlineConfig); diff --git a/packages/astro/src/core/sync/constants.ts b/packages/astro/src/core/sync/constants.ts new file mode 100644 index 000000000000..7ff603105a75 --- /dev/null +++ b/packages/astro/src/core/sync/constants.ts @@ -0,0 +1,2 @@ +// TODO: use types.d.ts for backward compatibility. Use astro.d.ts in Astro 5.0 +export const REFERENCE_FILE = './types.d.ts'; diff --git a/packages/astro/src/core/sync/index.ts b/packages/astro/src/core/sync/index.ts index 909974eddeb3..c9b2ec235b98 100644 --- a/packages/astro/src/core/sync/index.ts +++ b/packages/astro/src/core/sync/index.ts @@ -1,10 +1,8 @@ import fsMod, { existsSync } from 'node:fs'; import { performance } from 'node:perf_hooks'; -import { fileURLToPath } from 'node:url'; import { dim } from 'kleur/colors'; import { type HMRPayload, createServer } from 'vite'; import type { AstroConfig, AstroInlineConfig, AstroSettings } from '../../@types/astro.js'; -import { getPackage } from '../../cli/install-package.js'; import { DATA_STORE_FILE } from '../../content/consts.js'; import { globalContentLayer } from '../../content/content-layer.js'; import { DataStore, globalDataStore } from '../../content/data-store.js'; @@ -13,7 +11,7 @@ import { globalContentConfigObserver } from '../../content/utils.js'; import { syncAstroEnv } from '../../env/sync.js'; import { telemetry } from '../../events/index.js'; import { eventCliSession } from '../../events/session.js'; -import { runHookConfigSetup } from '../../integrations/hooks.js'; +import { runHookConfigDone, runHookConfigSetup } from '../../integrations/hooks.js'; import { getTimeStat } from '../build/util.js'; import { resolveConfig } from '../config/config.js'; import { createNodeLogger } from '../config/logging.js'; @@ -30,7 +28,7 @@ import { import type { Logger } from '../logger/core.js'; import { formatErrorMessage } from '../messages.js'; import { ensureProcessNodeEnv } from '../util.js'; -import { setUpEnvTs } from './setup-env-ts.js'; +import { writeFiles } from './write-files.js'; export type SyncOptions = { /** @@ -46,15 +44,10 @@ export type SyncOptions = { }; }; -type DBPackage = { - typegen?: (args: Pick) => Promise; -}; - -export default async function sync({ - inlineConfig, - fs, - telemetry: _telemetry = false, -}: { inlineConfig: AstroInlineConfig; fs?: typeof fsMod; telemetry?: boolean }) { +export default async function sync( + inlineConfig: AstroInlineConfig, + { fs, telemetry: _telemetry = false }: { fs?: typeof fsMod; telemetry?: boolean } = {}, +) { ensureProcessNodeEnv('production'); const logger = createNodeLogger(inlineConfig); const { astroConfig, userConfig } = await resolveConfig(inlineConfig ?? {}, 'sync'); @@ -67,6 +60,7 @@ export default async function sync({ settings, logger, }); + await runHookConfigDone({ settings, logger }); return await syncInternal({ settings, logger, fs, force: inlineConfig.force }); } @@ -103,21 +97,9 @@ export async function syncInternal({ await clearContentLayerCache({ astroConfig: settings.config, logger, fs }); } - const cwd = fileURLToPath(settings.config.root); - const timerStart = performance.now(); - const dbPackage = await getPackage( - '@astrojs/db', - logger, - { - optional: true, - cwd, - }, - [], - ); try { - await dbPackage?.typegen?.(settings.config); if (!skip?.content) { await syncContentCollections(settings, { fs, logger }); settings.timer.start('Sync content layer'); @@ -145,7 +127,7 @@ export async function syncInternal({ } syncAstroEnv(settings, fs); - await setUpEnvTs({ settings, logger, fs }); + await writeFiles(settings, fs, logger); logger.info('types', `Generated ${dim(getTimeStat(timerStart, performance.now()))}`); } catch (err) { const error = createSafeError(err); diff --git a/packages/astro/src/core/sync/setup-env-ts.ts b/packages/astro/src/core/sync/setup-env-ts.ts deleted file mode 100644 index 39eb062e5bcd..000000000000 --- a/packages/astro/src/core/sync/setup-env-ts.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type fsMod from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { bold } from 'kleur/colors'; -import { normalizePath } from 'vite'; -import type { AstroSettings } from '../../@types/astro.js'; -import { ACTIONS_TYPES_FILE } from '../../actions/consts.js'; -import { CONTENT_TYPES_FILE } from '../../content/consts.js'; -import { ENV_TYPES_FILE } from '../../env/constants.js'; -import { type Logger } from '../logger/core.js'; - -function getDotAstroTypeReference({ - settings, - filename, -}: { settings: AstroSettings; filename: string }) { - const relativePath = normalizePath( - path.relative( - fileURLToPath(settings.config.srcDir), - fileURLToPath(new URL(filename, settings.dotAstroDir)), - ), - ); - - return `/// `; -} - -type InjectedType = { filename: string; meetsCondition?: () => boolean | Promise }; - -export async function setUpEnvTs({ - settings, - logger, - fs, -}: { - settings: AstroSettings; - logger: Logger; - fs: typeof fsMod; -}) { - const envTsPath = new URL('env.d.ts', settings.config.srcDir); - const envTsPathRelativetoRoot = normalizePath( - path.relative(fileURLToPath(settings.config.root), fileURLToPath(envTsPath)), - ); - - const injectedTypes: Array = [ - { - filename: CONTENT_TYPES_FILE, - meetsCondition: () => fs.existsSync(new URL(CONTENT_TYPES_FILE, settings.dotAstroDir)), - }, - { - filename: ACTIONS_TYPES_FILE, - meetsCondition: () => fs.existsSync(new URL(ACTIONS_TYPES_FILE, settings.dotAstroDir)), - }, - ]; - if (settings.config.experimental.env) { - injectedTypes.push({ - filename: ENV_TYPES_FILE, - }); - } - - if (fs.existsSync(envTsPath)) { - const initialEnvContents = await fs.promises.readFile(envTsPath, 'utf-8'); - let typesEnvContents = initialEnvContents; - - for (const injectedType of injectedTypes) { - if (!injectedType.meetsCondition || (await injectedType.meetsCondition?.())) { - const expectedTypeReference = getDotAstroTypeReference({ - settings, - filename: injectedType.filename, - }); - - if (!typesEnvContents.includes(expectedTypeReference)) { - typesEnvContents = `${expectedTypeReference}\n${typesEnvContents}`; - } - } - } - - if (initialEnvContents !== typesEnvContents) { - logger.info('types', `Updated ${bold(envTsPathRelativetoRoot)} type declarations.`); - await fs.promises.writeFile(envTsPath, typesEnvContents, 'utf-8'); - } - } else { - // Otherwise, inject the `env.d.ts` file - let referenceDefs: string[] = []; - referenceDefs.push('/// '); - - for (const injectedType of injectedTypes) { - if (!injectedType.meetsCondition || (await injectedType.meetsCondition?.())) { - referenceDefs.push(getDotAstroTypeReference({ settings, filename: injectedType.filename })); - } - } - - await fs.promises.mkdir(settings.config.srcDir, { recursive: true }); - await fs.promises.writeFile(envTsPath, referenceDefs.join('\n'), 'utf-8'); - logger.info('types', `Added ${bold(envTsPathRelativetoRoot)} type declarations`); - } -} diff --git a/packages/astro/src/core/sync/write-files.ts b/packages/astro/src/core/sync/write-files.ts new file mode 100644 index 000000000000..56ab131f1be7 --- /dev/null +++ b/packages/astro/src/core/sync/write-files.ts @@ -0,0 +1,78 @@ +import type fsMod from 'node:fs'; +import { dirname, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { bold } from 'kleur/colors'; +import { normalizePath } from 'vite'; +import type { AstroSettings } from '../../@types/astro.js'; +import type { Logger } from '../logger/core.js'; +import { REFERENCE_FILE } from './constants.js'; +import { AstroError, AstroErrorData } from '../errors/index.js'; + +export async function writeFiles(settings: AstroSettings, fs: typeof fsMod, logger: Logger) { + try { + writeInjectedTypes(settings, fs); + await setUpEnvTs(settings, fs, logger); + } catch (e) { + throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: e }); + } +} + +function getTsReference(type: 'path' | 'types', value: string) { + return `/// `; +} + +const CLIENT_TYPES_REFERENCE = getTsReference('types', 'astro/client'); + +function writeInjectedTypes(settings: AstroSettings, fs: typeof fsMod) { + const references: Array = []; + + for (const { filename, content } of settings.injectedTypes) { + const filepath = normalizePath(fileURLToPath(new URL(filename, settings.dotAstroDir))); + fs.mkdirSync(dirname(filepath), { recursive: true }); + fs.writeFileSync(filepath, content, 'utf-8'); + references.push(normalizePath(relative(fileURLToPath(settings.dotAstroDir), filepath))); + } + + const astroDtsContent = `${CLIENT_TYPES_REFERENCE}\n${references.map((reference) => getTsReference('path', reference)).join('\n')}`; + if (references.length === 0) { + fs.mkdirSync(settings.dotAstroDir, { recursive: true }); + } + fs.writeFileSync( + normalizePath(fileURLToPath(new URL(REFERENCE_FILE, settings.dotAstroDir))), + astroDtsContent, + 'utf-8', + ); +} + +async function setUpEnvTs(settings: AstroSettings, fs: typeof fsMod, logger: Logger) { + const envTsPath = normalizePath(fileURLToPath(new URL('env.d.ts', settings.config.srcDir))); + const envTsPathRelativetoRoot = normalizePath( + relative(fileURLToPath(settings.config.root), envTsPath), + ); + const relativePath = normalizePath( + relative( + fileURLToPath(settings.config.srcDir), + fileURLToPath(new URL(REFERENCE_FILE, settings.dotAstroDir)), + ), + ); + const expectedTypeReference = getTsReference('path', relativePath); + + if (fs.existsSync(envTsPath)) { + const initialEnvContents = await fs.promises.readFile(envTsPath, 'utf-8'); + let typesEnvContents = initialEnvContents; + + if (!typesEnvContents.includes(expectedTypeReference)) { + typesEnvContents = `${expectedTypeReference}\n${typesEnvContents}`; + } + + if (initialEnvContents !== typesEnvContents) { + logger.info('types', `Updated ${bold(envTsPathRelativetoRoot)} type declarations.`); + await fs.promises.writeFile(envTsPath, typesEnvContents, 'utf-8'); + } + } else { + // Otherwise, inject the `env.d.ts` file + await fs.promises.mkdir(settings.config.srcDir, { recursive: true }); + await fs.promises.writeFile(envTsPath, expectedTypeReference, 'utf-8'); + logger.info('types', `Added ${bold(envTsPathRelativetoRoot)} type declarations`); + } +} diff --git a/packages/astro/src/env/sync.ts b/packages/astro/src/env/sync.ts index 9ba11469ad7a..27436f967be7 100644 --- a/packages/astro/src/env/sync.ts +++ b/packages/astro/src/env/sync.ts @@ -1,9 +1,9 @@ import fsMod from 'node:fs'; import type { AstroSettings } from '../@types/astro.js'; -import { ENV_TYPES_FILE, TYPES_TEMPLATE_URL } from './constants.js'; +import { TYPES_TEMPLATE_URL } from './constants.js'; import { getEnvFieldType } from './validators.js'; -export function syncAstroEnv(settings: AstroSettings, fs = fsMod) { +export function syncAstroEnv(settings: AstroSettings, fs = fsMod): void { if (!settings.config.experimental.env) { return; } @@ -23,8 +23,10 @@ export function syncAstroEnv(settings: AstroSettings, fs = fsMod) { } const template = fs.readFileSync(TYPES_TEMPLATE_URL, 'utf-8'); - const dts = template.replace('// @@CLIENT@@', client).replace('// @@SERVER@@', server); + const content = template.replace('// @@CLIENT@@', client).replace('// @@SERVER@@', server); - fs.mkdirSync(settings.dotAstroDir, { recursive: true }); - fs.writeFileSync(new URL(ENV_TYPES_FILE, settings.dotAstroDir), dts, 'utf-8'); + settings.injectedTypes.push({ + filename: 'astro/env.d.ts', + content, + }); } diff --git a/packages/astro/src/integrations/hooks.ts b/packages/astro/src/integrations/hooks.ts index 9b2859e48b3c..36682fe2b256 100644 --- a/packages/astro/src/integrations/hooks.ts +++ b/packages/astro/src/integrations/hooks.ts @@ -100,6 +100,18 @@ export function getToolbarServerCommunicationHelpers(server: ViteDevServer) { }; } +// Will match any invalid characters (will be converted to _). We only allow a-zA-Z0-9.-_ +const SAFE_CHARS_RE = /[^\w.-]/g; + +export function normalizeInjectedTypeFilename(filename: string, integrationName: string): string { + if (!filename.endsWith('.d.ts')) { + throw new Error( + `Integration ${bold(integrationName)} is injecting a type that does not end with "${bold('.d.ts')}"`, + ); + } + return `./integrations/${integrationName.replace(SAFE_CHARS_RE, '_')}/${filename.replace(SAFE_CHARS_RE, '_')}`; +} + export async function runHookConfigSetup({ settings, command, @@ -327,6 +339,19 @@ export async function runHookConfigDone({ } settings.adapter = adapter; }, + injectTypes(injectedType) { + const normalizedFilename = normalizeInjectedTypeFilename( + injectedType.filename, + integration.name, + ); + + settings.injectedTypes.push({ + filename: normalizedFilename, + content: injectedType.content, + }); + + return new URL(normalizedFilename, settings.config.root); + }, logger: getLogger(integration, logger), }), logger, diff --git a/packages/astro/src/preferences/index.ts b/packages/astro/src/preferences/index.ts index 9318824bf67e..2b92c5fb6cb5 100644 --- a/packages/astro/src/preferences/index.ts +++ b/packages/astro/src/preferences/index.ts @@ -82,9 +82,9 @@ export function coerce(key: string, value: unknown) { return value as any; } -export default function createPreferences(config: AstroConfig): AstroPreferences { +export default function createPreferences(config: AstroConfig, dotAstroDir: URL): AstroPreferences { const global = new PreferenceStore(getGlobalPreferenceDir()); - const project = new PreferenceStore(fileURLToPath(new URL('./.astro/', config.root))); + const project = new PreferenceStore(fileURLToPath(dotAstroDir)); const stores: Record = { global, project }; return { diff --git a/packages/astro/test/astro-sync.test.js b/packages/astro/test/astro-sync.test.js index a92993eae368..dfe4755d1102 100644 --- a/packages/astro/test/astro-sync.test.js +++ b/packages/astro/test/astro-sync.test.js @@ -1,8 +1,10 @@ +// @ts-check import assert from 'node:assert/strict'; import * as fs from 'node:fs'; -import { before, describe, it } from 'node:test'; +import { beforeEach, describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; import ts from 'typescript'; +import { normalizePath } from 'vite'; import { loadFixture } from './test-utils.js'; const createFixture = () => { @@ -11,66 +13,79 @@ const createFixture = () => { /** @type {Record} */ const writtenFiles = {}; + /** + * @param {string} path + */ + const getExpectedPath = (path) => + normalizePath(fileURLToPath(new URL(path, astroFixture.config.root))); + return { /** @param {string} root */ - async whenSyncing(root) { + async load(root) { astroFixture = await loadFixture({ root }); - - const envPath = new URL('env.d.ts', astroFixture.config.srcDir).href; - const typesDtsPath = new URL('.astro/types.d.ts', astroFixture.config.root).href; - + return astroFixture.config; + }, + clean() { + const envPath = new URL('env.d.ts', astroFixture.config.srcDir); + if (fs.existsSync(envPath)) { + fs.unlinkSync(new URL('env.d.ts', astroFixture.config.srcDir)); + } + fs.rmSync(new URL('./.astro/', astroFixture.config.root), { force: true, recursive: true }); + }, + async whenSyncing() { const fsMock = { ...fs, - existsSync(path, ...args) { - if (path.toString() === envPath) { - return false; - } - if (path.toString() === typesDtsPath) { - return true; - } - return fs.existsSync(path, ...args); - }, + /** + * @param {fs.PathLike} path + * @param {string} contents + */ writeFileSync(path, contents) { writtenFiles[path.toString()] = contents; + return fs.writeFileSync(path, contents); }, promises: { ...fs.promises, - async readFile(path, ...args) { - if (path.toString() === envPath) { - return `/// `; - } else { - return fs.promises.readFile(path, ...args); - } - }, - async writeFile(path, contents) { + /** + * @param {fs.PathLike} path + * @param {string} contents + */ + writeFile(path, contents) { writtenFiles[path.toString()] = contents; + return fs.promises.writeFile(path, contents); }, }, }; - await astroFixture.sync({ - inlineConfig: { root: fileURLToPath(new URL(root, import.meta.url)) }, - fs: fsMock, - }); + await astroFixture.sync( + { root: fileURLToPath(astroFixture.config.root) }, + { + // @ts-ignore + fs: fsMock, + }, + ); }, /** @param {string} path */ thenFileShouldExist(path) { - const expectedPath = new URL(path, astroFixture.config.root).href; - assert.equal(writtenFiles.hasOwnProperty(expectedPath), true, `${path} does not exist`); + assert.equal( + writtenFiles.hasOwnProperty(getExpectedPath(path)), + true, + `${path} does not exist`, + ); }, /** * @param {string} path * @param {string} content * @param {string | undefined} error */ - thenFileContentShouldInclude(path, content, error) { - const expectedPath = new URL(path, astroFixture.config.root).href; - assert.equal(writtenFiles[expectedPath].includes(content), true, error); + thenFileContentShouldInclude(path, content, error = undefined) { + assert.equal(writtenFiles[getExpectedPath(path)].includes(content), true, error); }, + /** + * @param {string} path + */ thenFileShouldBeValidTypescript(path) { - const expectedPath = new URL(path, astroFixture.config.root).href; try { - const content = writtenFiles[expectedPath]; + const content = writtenFiles[getExpectedPath(path)]; const result = ts.transpileModule(content, { compilerOptions: { module: ts.ModuleKind.ESNext, @@ -91,33 +106,71 @@ const createFixture = () => { describe('astro sync', () => { /** @type {ReturnType} */ let fixture; - before(async () => { + beforeEach(async () => { fixture = createFixture(); }); - it('Writes `src/env.d.ts` if none exists', async () => { - await fixture.whenSyncing('./fixtures/astro-basic/'); - fixture.thenFileShouldExist('src/env.d.ts'); - fixture.thenFileContentShouldInclude('src/env.d.ts', `/// `); + describe('References', () => { + it('Writes `src/env.d.ts` if none exists', async () => { + await fixture.load('./fixtures/astro-basic/'); + fixture.clean(); + await fixture.whenSyncing(); + fixture.thenFileShouldExist('src/env.d.ts'); + fixture.thenFileContentShouldInclude( + 'src/env.d.ts', + `/// `, + ); + }); + + it('Updates `src/env.d.ts` if one exists', async () => { + const config = await fixture.load('./fixtures/astro-basic/'); + fixture.clean(); + fs.writeFileSync(new URL('./env.d.ts', config.srcDir), '// whatever', 'utf-8'); + await fixture.whenSyncing(); + fixture.thenFileShouldExist('src/env.d.ts'); + fixture.thenFileContentShouldInclude( + 'src/env.d.ts', + `/// `, + ); + }); + + it('Writes `src/types.d.ts`', async () => { + await fixture.load('./fixtures/astro-basic/'); + fixture.clean(); + await fixture.whenSyncing(); + fixture.thenFileShouldExist('.astro/types.d.ts'); + fixture.thenFileContentShouldInclude( + '.astro/types.d.ts', + `/// `, + ); + }); }); describe('Content collections', () => { - it('Writes types to `.astro`', async () => { - await fixture.whenSyncing('./fixtures/content-collections/'); + it('Adds reference to `.astro/types.d.ts`', async () => { + await fixture.load('./fixtures/content-collections/'); + fixture.clean(); + await fixture.whenSyncing(); fixture.thenFileShouldExist('.astro/types.d.ts'); fixture.thenFileContentShouldInclude( '.astro/types.d.ts', + `/// `, + ); + fixture.thenFileShouldExist('.astro/astro/content.d.ts'); + fixture.thenFileContentShouldInclude( + '.astro/astro/content.d.ts', `declare module 'astro:content' {`, 'Types file does not include `astro:content` module declaration', ); - fixture.thenFileShouldBeValidTypescript('.astro/types.d.ts'); + fixture.thenFileShouldBeValidTypescript('.astro/astro/content.d.ts'); }); it('Writes types for empty collections', async () => { - await fixture.whenSyncing('./fixtures/content-collections-empty-dir/'); - fixture.thenFileShouldExist('.astro/types.d.ts'); + await fixture.load('./fixtures/content-collections-empty-dir/'); + fixture.clean(); + await fixture.whenSyncing(); fixture.thenFileContentShouldInclude( - '.astro/types.d.ts', + '.astro/astro/content.d.ts', `"blog": Record { 'Types file does not include empty collection type', ); fixture.thenFileContentShouldInclude( - '.astro/types.d.ts', + '.astro/astro/content.d.ts', `"blogMeta": Record { 'Types file does not include empty collection type', ); }); + }); - it('Adds type reference to `src/env.d.ts`', async () => { - await fixture.whenSyncing('./fixtures/content-collections/'); - fixture.thenFileShouldExist('src/env.d.ts'); + describe('astro:env', () => { + it('Adds reference to `.astro/types.d.ts`', async () => { + await fixture.load('./fixtures/astro-env/'); + fixture.clean(); + await fixture.whenSyncing(); + fixture.thenFileShouldExist('.astro/types.d.ts'); fixture.thenFileContentShouldInclude( - 'src/env.d.ts', - `/// `, + '.astro/types.d.ts', + `/// `, ); - }); - }); - - describe('Astro Env', () => { - it('Writes types to `.astro`', async () => { - await fixture.whenSyncing('./fixtures/astro-env/'); - fixture.thenFileShouldExist('.astro/env.d.ts'); + fixture.thenFileShouldExist('.astro/astro/env.d.ts'); fixture.thenFileContentShouldInclude( - '.astro/env.d.ts', + '.astro/astro/env.d.ts', `declare module 'astro:env/client' {`, 'Types file does not include `astro:env` module declaration', ); }); - it('Adds type reference to `src/env.d.ts`', async () => { - await fixture.whenSyncing('./fixtures/astro-env/'); - fixture.thenFileShouldExist('src/env.d.ts'); - fixture.thenFileContentShouldInclude( - 'src/env.d.ts', - `/// `, - ); - }); - it('Does not throw if a public variable is required', async () => { - let error = null; try { - await fixture.whenSyncing('./fixtures/astro-env-required-public/'); - } catch (e) { - error = e; + await fixture.load('./fixtures/astro-env-required-public/'); + fixture.clean(); + await fixture.whenSyncing(); + assert.ok(true); + } catch { + assert.fail(); } - - assert.equal(error, null, 'Syncing should not throw astro:env validation errors'); }); }); - describe('Astro Actions', () => { - // We can't check for the file existence or content yet because - // it's an integration and does not use the fs mock - - it('Adds type reference to `src/env.d.ts`', async () => { - await fixture.whenSyncing('./fixtures/actions/'); - fixture.thenFileShouldExist('src/env.d.ts'); + describe('astro:actions', () => { + it('Adds reference to `.astro/types.d.ts`', async () => { + await fixture.load('./fixtures/actions/'); + fixture.clean(); + await fixture.whenSyncing(); + fixture.thenFileShouldExist('.astro/types.d.ts'); fixture.thenFileContentShouldInclude( - 'src/env.d.ts', - `/// `, + '.astro/types.d.ts', + `/// `, + ); + fixture.thenFileShouldExist('.astro/astro/actions.d.ts'); + fixture.thenFileContentShouldInclude( + '.astro/astro/actions.d.ts', + `declare module "astro:actions" {`, + 'Types file does not include `astro:actions` module declaration', ); + fixture.thenFileShouldBeValidTypescript('.astro/astro/actions.d.ts'); }); }); }); diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index 2fcd840a8a54..95edeebd2673 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -24,6 +24,7 @@ process.env.ASTRO_TELEMETRY_DISABLED = true; /** * @typedef {import('../src/core/dev/dev').DevServer} DevServer * @typedef {import('../src/@types/astro').AstroInlineConfig & { root?: string | URL }} AstroInlineConfig + * @typedef {import('../src/@types/astro').AstroConfig} AstroConfig * @typedef {import('../src/core/preview/index').PreviewServer} PreviewServer * @typedef {import('../src/core/app/index').App} App * @typedef {import('../src/cli/check/index').AstroChecker} AstroChecker @@ -49,6 +50,7 @@ process.env.ASTRO_TELEMETRY_DISABLED = true; * @property {() => Promise} onNextChange * @property {typeof check} check * @property {typeof sync} sync + * @property {AstroConfig} config * * This function returns an instance of the Check * diff --git a/packages/astro/test/units/dev/collections-mixed-content-errors.test.js b/packages/astro/test/units/dev/collections-mixed-content-errors.test.js index 73ce15377f34..3417650fc963 100644 --- a/packages/astro/test/units/dev/collections-mixed-content-errors.test.js +++ b/packages/astro/test/units/dev/collections-mixed-content-errors.test.js @@ -8,13 +8,15 @@ const root = new URL('../../fixtures/content-mixed-errors/', import.meta.url); async function sync({ fs }) { try { - await _sync({ - inlineConfig: { + await _sync( + { root: fileURLToPath(root), logLevel: 'silent', }, - fs, - }); + { + fs, + }, + ); return 0; } catch (_) { return 1; @@ -121,11 +123,7 @@ title: Post root, ); - try { - const res = await sync({ fs }); - assert.equal(res, 0); - } catch (e) { - assert.fail(`Did not expect sync to throw: ${e.message}`); - } + const res = await sync({ fs }); + assert.equal(res, 0); }); }); diff --git a/packages/astro/test/units/dev/restart.test.js b/packages/astro/test/units/dev/restart.test.js index a09bfce98c6f..339b95fc1acb 100644 --- a/packages/astro/test/units/dev/restart.test.js +++ b/packages/astro/test/units/dev/restart.test.js @@ -194,4 +194,31 @@ describe('dev container restarts', () => { await restart.container.close(); } }); + + it('Is able to restart project on .astro/settings.json changes', async () => { + const fs = createFs( + { + '/src/pages/index.astro': ``, + '/.astro/settings.json': `{}`, + }, + root, + ); + + const restart = await createContainerWithAutomaticRestart({ + fs, + inlineConfig: { root: fileURLToPath(root), logLevel: 'silent' }, + }); + await startContainer(restart.container); + assert.equal(isStarted(restart.container), true); + + try { + let restartComplete = restart.restarted(); + fs.mkdirSync('/.astro/', { recursive: true }); + fs.writeFileSync('/.astro/settings.json', `{ }`); + triggerFSEvent(restart.container, fs, '/.astro/settings.json', 'change'); + await restartComplete; + } finally { + await restart.container.close(); + } + }); }); diff --git a/packages/astro/test/units/integrations/api.test.js b/packages/astro/test/units/integrations/api.test.js index 960e003fc3f1..6122ba6408f6 100644 --- a/packages/astro/test/units/integrations/api.test.js +++ b/packages/astro/test/units/integrations/api.test.js @@ -1,7 +1,11 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { validateSupportedFeatures } from '../../../dist/integrations/features-validation.js'; -import { runHookBuildSetup, runHookConfigSetup } from '../../../dist/integrations/hooks.js'; +import { + normalizeInjectedTypeFilename, + runHookBuildSetup, + runHookConfigSetup, +} from '../../../dist/integrations/hooks.js'; import { defaultLogger } from '../test-utils.js'; describe('Integration API', () => { @@ -311,3 +315,20 @@ describe('Astro feature map', function () { }); }); }); + +describe('normalizeInjectedTypeFilename', () => { + // invalid filename + assert.throws(() => normalizeInjectedTypeFilename('types', 'integration')); + // valid filename + assert.doesNotThrow(() => normalizeInjectedTypeFilename('types.d.ts', 'integration')); + // filename normalization + assert.equal( + normalizeInjectedTypeFilename('aA1-*/_"~.d.ts', 'integration'), + './integrations/integration/aA1-_____.d.ts', + ); + // integration name normalization + assert.equal( + normalizeInjectedTypeFilename('types.d.ts', 'aA1-*/_"~.'), + './integrations/aA1-_____./types.d.ts', + ); +}); diff --git a/packages/db/src/core/integration/index.ts b/packages/db/src/core/integration/index.ts index 19c712c802aa..054c74d74156 100644 --- a/packages/db/src/core/integration/index.ts +++ b/packages/db/src/core/integration/index.ts @@ -22,7 +22,7 @@ import { resolveDbConfig } from '../load-file.js'; import { SEED_DEV_FILE_NAME } from '../queries.js'; import { type VitePlugin, getDbDirectoryUrl } from '../utils.js'; import { fileURLIntegration } from './file-url.js'; -import { typegenInternal } from './typegen.js'; +import { getDtsContent } from './typegen.js'; import { type LateSeedFiles, type LateTables, @@ -30,7 +30,6 @@ import { resolved, vitePluginDb, } from './vite-plugin-db.js'; -import { vitePluginInjectEnvTs } from './vite-plugin-inject-env-ts.js'; function astroDBIntegration(): AstroIntegration { let connectToStudio = false; @@ -102,11 +101,11 @@ function astroDBIntegration(): AstroIntegration { updateConfig({ vite: { assetsInclude: [DB_PATH], - plugins: [dbPlugin, vitePluginInjectEnvTs(config, logger)], + plugins: [dbPlugin], }, }); }, - 'astro:config:done': async ({ config }) => { + 'astro:config:done': async ({ config, injectTypes }) => { if (command === 'preview') return; // TODO: refine where we load tables @@ -122,7 +121,10 @@ function astroDBIntegration(): AstroIntegration { await writeFile(localDbUrl, ''); } - await typegenInternal({ tables: tables.get() ?? {}, root: config.root }); + injectTypes({ + filename: 'db.d.ts', + content: getDtsContent(tables.get() ?? {}), + }); }, 'astro:server:setup': async ({ server, logger }) => { seedHandler.execute = async (fileUrl) => { diff --git a/packages/db/src/core/integration/typegen.ts b/packages/db/src/core/integration/typegen.ts index 725738fc5a51..91364b3c3dc1 100644 --- a/packages/db/src/core/integration/typegen.ts +++ b/packages/db/src/core/integration/typegen.ts @@ -1,18 +1,7 @@ -import { existsSync } from 'node:fs'; -import { mkdir, writeFile } from 'node:fs/promises'; -import type { AstroConfig } from 'astro'; -import { DB_TYPES_FILE, RUNTIME_IMPORT } from '../consts.js'; -import { resolveDbConfig } from '../load-file.js'; +import { RUNTIME_IMPORT } from '../consts.js'; import type { DBTable, DBTables } from '../types.js'; -// Exported for use in Astro core CLI -export async function typegen(astroConfig: Pick) { - const { dbConfig } = await resolveDbConfig(astroConfig); - - await typegenInternal({ tables: dbConfig.tables, root: astroConfig.root }); -} - -export async function typegenInternal({ tables, root }: { tables: DBTables; root: URL }) { +export function getDtsContent(tables: DBTables) { const content = `// This file is generated by Astro DB declare module 'astro:db' { ${Object.entries(tables) @@ -20,14 +9,7 @@ ${Object.entries(tables) .join('\n')} } `; - - const dotAstroDir = new URL('.astro/', root); - - if (!existsSync(dotAstroDir)) { - await mkdir(dotAstroDir); - } - - await writeFile(new URL(DB_TYPES_FILE, dotAstroDir), content); + return content; } function generateTableType(name: string, table: DBTable): string { diff --git a/packages/db/src/core/integration/vite-plugin-inject-env-ts.ts b/packages/db/src/core/integration/vite-plugin-inject-env-ts.ts deleted file mode 100644 index 14257d480fef..000000000000 --- a/packages/db/src/core/integration/vite-plugin-inject-env-ts.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { existsSync } from 'node:fs'; -import { readFile, writeFile } from 'node:fs/promises'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import type { AstroIntegrationLogger } from 'astro'; -import { bold } from 'kleur/colors'; -import { normalizePath } from 'vite'; -import { DB_TYPES_FILE } from '../consts.js'; -import type { VitePlugin } from '../utils.js'; - -export function vitePluginInjectEnvTs( - { srcDir, root }: { srcDir: URL; root: URL }, - logger: AstroIntegrationLogger, -): VitePlugin { - return { - name: 'db-inject-env-ts', - enforce: 'post', - async config() { - await setUpEnvTs({ srcDir, root, logger }); - }, - }; -} - -export async function setUpEnvTs({ - srcDir, - root, - logger, -}: { - srcDir: URL; - root: URL; - logger: AstroIntegrationLogger; -}) { - const envTsPath = getEnvTsPath({ srcDir }); - const envTsPathRelativetoRoot = normalizePath( - path.relative(fileURLToPath(root), fileURLToPath(envTsPath)), - ); - - if (existsSync(envTsPath)) { - let typesEnvContents = await readFile(envTsPath, 'utf-8'); - const dotAstroDir = new URL('.astro/', root); - - if (!existsSync(dotAstroDir)) return; - - const dbTypeReference = getDBTypeReference({ srcDir, dotAstroDir }); - - if (!typesEnvContents.includes(dbTypeReference)) { - typesEnvContents = `${dbTypeReference}\n${typesEnvContents}`; - await writeFile(envTsPath, typesEnvContents, 'utf-8'); - logger.info(`Added ${bold(envTsPathRelativetoRoot)} types`); - } - } -} - -function getDBTypeReference({ srcDir, dotAstroDir }: { srcDir: URL; dotAstroDir: URL }) { - const dbTypesFile = new URL(DB_TYPES_FILE, dotAstroDir); - const contentTypesRelativeToSrcDir = normalizePath( - path.relative(fileURLToPath(srcDir), fileURLToPath(dbTypesFile)), - ); - - return `/// `; -} - -function getEnvTsPath({ srcDir }: { srcDir: URL }) { - return new URL('env.d.ts', srcDir); -} diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index b26507130471..f7022a24a236 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1,4 +1,3 @@ export type { TableConfig } from './core/types.js'; export { cli } from './core/cli/index.js'; export { integration as default } from './core/integration/index.js'; -export { typegen } from './core/integration/typegen.js';