0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-01-13 22:11:20 -05:00

feat(next): codegenDir and update .astro paths (#11963)

Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
Florian Lefebvre 2024-09-26 17:13:17 +02:00 committed by GitHub
parent a19530e377
commit 0a1036eef6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 87 additions and 42 deletions

View file

@ -0,0 +1,25 @@
---
'astro': minor
---
Adds a new `createCodegenDir()` function to the `astro:config:setup` hook in the Integrations API
In 4.14, we introduced the `injectTypes` utility on the `astro:config:done` hook. It can create `.d.ts` files and make their types available to user's projects automatically. Under the hood, it creates a file in `<root>/.astro/integrations/<normalized_integration_name>`.
While the `.astro` directory has always been the preferred place to write code generated files, it has also been prone to mistakes. For example, you can write a `.astro/types.d.ts` file, breaking Astro types. Or you can create a file that overrides a file created by another integration.
In this release, `<root>/.astro/integrations/<normalized_integration_name>` can now be retrieved in the `astro:config:setup` hook by calling `createCodegenDir()`. It allows you to have a dedicated folder, avoiding conflicts with another integration or Astro itself. This directory is created by calling this function so it's safe to write files to it directly:
```js
import { writeFileSync } from 'node:fs'
const integration = {
name: 'my-integration',
hooks: {
'astro:config:setup': ({ createCodegenDir }) => {
const codegenDir = createCodegenDir()
writeFileSync(new URL('cache.json', codegenDir), '{}', 'utf-8')
}
}
}
```

View file

@ -1,6 +1,6 @@
export const VIRTUAL_MODULE_ID = 'astro:actions'; export const VIRTUAL_MODULE_ID = 'astro:actions';
export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID; export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
export const ACTIONS_TYPES_FILE = 'astro/actions.d.ts'; export const ACTIONS_TYPES_FILE = 'actions.d.ts';
export const VIRTUAL_INTERNAL_MODULE_ID = 'astro:internal-actions'; export const VIRTUAL_INTERNAL_MODULE_ID = 'astro:internal-actions';
export const RESOLVED_VIRTUAL_INTERNAL_MODULE_ID = '\0astro:internal-actions'; export const RESOLVED_VIRTUAL_INTERNAL_MODULE_ID = '\0astro:internal-actions';
export const NOOP_ACTIONS = '\0noop-actions'; export const NOOP_ACTIONS = '\0noop-actions';

View file

@ -33,10 +33,11 @@ export const CONTENT_FLAGS = [
CONTENT_MODULE_FLAG, CONTENT_MODULE_FLAG,
] as const; ] as const;
export const CONTENT_TYPES_FILE = 'astro/content.d.ts'; export const CONTENT_TYPES_FILE = 'content.d.ts';
export const DATA_STORE_FILE = 'data-store.json'; export const DATA_STORE_FILE = 'data-store.json';
export const ASSET_IMPORTS_FILE = 'assets.mjs'; export const ASSET_IMPORTS_FILE = 'content-assets.mjs';
export const MODULES_IMPORTS_FILE = 'modules.mjs'; export const MODULES_IMPORTS_FILE = 'content-modules.mjs';
export const COLLECTIONS_MANIFEST_FILE = 'collections/collections.json';
export const COLLECTIONS_DIR = 'collections/'
export const CONTENT_LAYER_TYPE = 'content_layer'; export const CONTENT_LAYER_TYPE = 'content_layer';

View file

@ -8,6 +8,7 @@ import type { AstroSettings } from '../types/astro.js';
import type { ContentEntryType, RefreshContentOptions } from '../types/public/content.js'; import type { ContentEntryType, RefreshContentOptions } from '../types/public/content.js';
import { import {
ASSET_IMPORTS_FILE, ASSET_IMPORTS_FILE,
COLLECTIONS_MANIFEST_FILE,
CONTENT_LAYER_TYPE, CONTENT_LAYER_TYPE,
DATA_STORE_FILE, DATA_STORE_FILE,
MODULES_IMPORTS_FILE, MODULES_IMPORTS_FILE,
@ -214,14 +215,10 @@ export class ContentLayer {
return collection.loader.load(context); return collection.loader.load(context);
}), }),
); );
if (!existsSync(this.#settings.config.cacheDir)) { await fs.mkdir(this.#settings.config.cacheDir, { recursive: true });
await fs.mkdir(this.#settings.config.cacheDir, { recursive: true }); await fs.mkdir(this.#settings.dotAstroDir, { recursive: true });
}
const cacheFile = getDataStoreFile(this.#settings); const cacheFile = getDataStoreFile(this.#settings);
await this.#store.writeToDisk(cacheFile); await this.#store.writeToDisk(cacheFile);
if (!existsSync(this.#settings.dotAstroDir)) {
await fs.mkdir(this.#settings.dotAstroDir, { recursive: true });
}
const assetImportsFile = new URL(ASSET_IMPORTS_FILE, this.#settings.dotAstroDir); const assetImportsFile = new URL(ASSET_IMPORTS_FILE, this.#settings.dotAstroDir);
await this.#store.writeAssetImports(assetImportsFile); await this.#store.writeAssetImports(assetImportsFile);
const modulesImportsFile = new URL(MODULES_IMPORTS_FILE, this.#settings.dotAstroDir); const modulesImportsFile = new URL(MODULES_IMPORTS_FILE, this.#settings.dotAstroDir);
@ -233,7 +230,7 @@ export class ContentLayer {
} }
async regenerateCollectionFileManifest() { async regenerateCollectionFileManifest() {
const collectionsManifest = new URL('collections/collections.json', this.#settings.dotAstroDir); const collectionsManifest = new URL(COLLECTIONS_MANIFEST_FILE, this.#settings.dotAstroDir);
this.#logger.debug('content', 'Regenerating collection file manifest'); this.#logger.debug('content', 'Regenerating collection file manifest');
if (existsSync(collectionsManifest)) { if (existsSync(collectionsManifest)) {
try { try {

View file

@ -13,7 +13,12 @@ import type { Logger } from '../core/logger/core.js';
import { isRelativePath } from '../core/path.js'; import { isRelativePath } from '../core/path.js';
import type { AstroSettings } from '../types/astro.js'; import type { AstroSettings } from '../types/astro.js';
import type { ContentEntryType } from '../types/public/content.js'; import type { ContentEntryType } from '../types/public/content.js';
import { CONTENT_LAYER_TYPE, CONTENT_TYPES_FILE, VIRTUAL_MODULE_ID } from './consts.js'; import {
COLLECTIONS_DIR,
CONTENT_LAYER_TYPE,
CONTENT_TYPES_FILE,
VIRTUAL_MODULE_ID,
} from './consts.js';
import { import {
type CollectionConfig, type CollectionConfig,
type ContentConfig, type ContentConfig,
@ -428,10 +433,8 @@ async function writeContentFiles({
let contentTypesStr = ''; let contentTypesStr = '';
let dataTypesStr = ''; let dataTypesStr = '';
const collectionSchemasDir = new URL('./collections/', settings.dotAstroDir); const collectionSchemasDir = new URL(COLLECTIONS_DIR, settings.dotAstroDir);
if (!fs.existsSync(collectionSchemasDir)) { fs.mkdirSync(collectionSchemasDir, { recursive: true });
fs.mkdirSync(collectionSchemasDir, { recursive: true });
}
for (const [collection, config] of Object.entries(contentConfig?.collections ?? {})) { for (const [collection, config] of Object.entries(contentConfig?.collections ?? {})) {
collectionEntryMap[JSON.stringify(collection)] ??= { collectionEntryMap[JSON.stringify(collection)] ??= {
@ -568,12 +571,8 @@ async function writeContentFiles({
); );
} }
if (!fs.existsSync(settings.dotAstroDir)) {
fs.mkdirSync(settings.dotAstroDir, { recursive: true });
}
const configPathRelativeToCacheDir = normalizeConfigPath( const configPathRelativeToCacheDir = normalizeConfigPath(
new URL('astro', settings.dotAstroDir).pathname, settings.dotAstroDir.pathname,
contentPaths.config.url.pathname, contentPaths.config.url.pathname,
); );
@ -591,9 +590,11 @@ async function writeContentFiles({
// 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 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)) { if (settings.injectedTypes.some((t) => t.filename === CONTENT_TYPES_FILE)) {
const filePath = fileURLToPath(new URL(CONTENT_TYPES_FILE, settings.dotAstroDir)); await fs.promises.writeFile(
await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); new URL(CONTENT_TYPES_FILE, settings.dotAstroDir),
await fs.promises.writeFile(filePath, typeTemplateContent, 'utf-8'); typeTemplateContent,
'utf-8',
);
} else { } else {
settings.injectedTypes.push({ settings.injectedTypes.push({
filename: CONTENT_TYPES_FILE, filename: CONTENT_TYPES_FILE,

View file

@ -12,6 +12,7 @@ import { createSafeError } from '../errors/index.js';
import { formatErrorMessage } from '../messages.js'; import { formatErrorMessage } from '../messages.js';
import type { Container } from './container.js'; import type { Container } from './container.js';
import { createContainer, startContainer } from './container.js'; import { createContainer, startContainer } from './container.js';
import { SETTINGS_FILE } from '../../preferences/constants.js';
async function createRestartedContainer( async function createRestartedContainer(
container: Container, container: Container,
@ -50,7 +51,7 @@ function shouldRestartContainer(
else { else {
shouldRestart = configRE.test(normalizedChangedFile); shouldRestart = configRE.test(normalizedChangedFile);
const settingsPath = vite.normalizePath( const settingsPath = vite.normalizePath(
fileURLToPath(new URL('settings.json', settings.dotAstroDir)), fileURLToPath(new URL(SETTINGS_FILE, settings.dotAstroDir)),
); );
if (settingsPath.endsWith(normalizedChangedFile)) { if (settingsPath.endsWith(normalizedChangedFile)) {
shouldRestart = settings.preferences.ignoreNextPreferenceReload ? false : true; shouldRestart = settings.preferences.ignoreNextPreferenceReload ? false : true;

View file

@ -5,7 +5,7 @@ export const VIRTUAL_MODULES_IDS = {
}; };
export const VIRTUAL_MODULES_IDS_VALUES = new Set(Object.values(VIRTUAL_MODULES_IDS)); export const VIRTUAL_MODULES_IDS_VALUES = new Set(Object.values(VIRTUAL_MODULES_IDS));
export const ENV_TYPES_FILE = 'astro/env.d.ts'; export const ENV_TYPES_FILE = 'env.d.ts';
const PKG_BASE = new URL('../../', import.meta.url); const PKG_BASE = new URL('../../', import.meta.url);
export const MODULE_TEMPLATE_URL = new URL('templates/env.mjs', PKG_BASE); export const MODULE_TEMPLATE_URL = new URL('templates/env.mjs', PKG_BASE);

View file

@ -112,13 +112,17 @@ export function getToolbarServerCommunicationHelpers(server: ViteDevServer) {
// Will match any invalid characters (will be converted to _). We only allow a-zA-Z0-9.-_ // Will match any invalid characters (will be converted to _). We only allow a-zA-Z0-9.-_
const SAFE_CHARS_RE = /[^\w.-]/g; const SAFE_CHARS_RE = /[^\w.-]/g;
export function normalizeCodegenDir(integrationName: string): string {
return `./integrations/${integrationName.replace(SAFE_CHARS_RE, '_')}/`;
}
export function normalizeInjectedTypeFilename(filename: string, integrationName: string): string { export function normalizeInjectedTypeFilename(filename: string, integrationName: string): string {
if (!filename.endsWith('.d.ts')) { if (!filename.endsWith('.d.ts')) {
throw new Error( throw new Error(
`Integration ${bold(integrationName)} is injecting a type that does not end with "${bold('.d.ts')}"`, `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, '_')}`; return `${normalizeCodegenDir(integrationName)}${filename.replace(SAFE_CHARS_RE, '_')}`;
} }
export async function runHookConfigSetup({ export async function runHookConfigSetup({
@ -234,6 +238,11 @@ export async function runHookConfigSetup({
); );
updatedSettings.middlewares[order].push(entrypoint); updatedSettings.middlewares[order].push(entrypoint);
}, },
createCodegenDir: () => {
const codegenDir = new URL(normalizeCodegenDir(integration.name), settings.dotAstroDir);
fs.mkdirSync(codegenDir, { recursive: true });
return codegenDir;
},
logger: integrationLogger, logger: integrationLogger,
}; };

View file

@ -0,0 +1 @@
export const SETTINGS_FILE = 'settings.json';

View file

@ -2,13 +2,14 @@ import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import dget from 'dlv'; import dget from 'dlv';
import { dset } from 'dset'; import { dset } from 'dset';
import { SETTINGS_FILE } from './constants.js';
export class PreferenceStore { export class PreferenceStore {
private file: string; private file: string;
constructor( constructor(
private dir: string, private dir: string,
filename = 'settings.json', filename = SETTINGS_FILE,
) { ) {
this.file = path.join(this.dir, filename); this.file = path.join(this.dir, filename);
} }

View file

@ -176,6 +176,7 @@ export interface BaseIntegrationHooks {
addClientDirective: (directive: ClientDirectiveConfig) => void; addClientDirective: (directive: ClientDirectiveConfig) => void;
addDevToolbarApp: (entrypoint: DevToolbarAppEntry) => void; addDevToolbarApp: (entrypoint: DevToolbarAppEntry) => void;
addMiddleware: (mid: AstroIntegrationMiddleware) => void; addMiddleware: (mid: AstroIntegrationMiddleware) => void;
createCodegenDir: () => URL;
logger: AstroIntegrationLogger; logger: AstroIntegrationLogger;
}) => void | Promise<void>; }) => void | Promise<void>;
'astro:config:done': (options: { 'astro:config:done': (options: {

View file

@ -123,15 +123,15 @@ describe('astro sync', () => {
fixture.thenFileShouldExist('.astro/types.d.ts'); fixture.thenFileShouldExist('.astro/types.d.ts');
fixture.thenFileContentShouldInclude( fixture.thenFileContentShouldInclude(
'.astro/types.d.ts', '.astro/types.d.ts',
`/// <reference path="astro/content.d.ts" />`, `/// <reference path="content.d.ts" />`,
); );
fixture.thenFileShouldExist('.astro/astro/content.d.ts'); fixture.thenFileShouldExist('.astro/content.d.ts');
fixture.thenFileContentShouldInclude( fixture.thenFileContentShouldInclude(
'.astro/astro/content.d.ts', '.astro/content.d.ts',
`declare module 'astro:content' {`, `declare module 'astro:content' {`,
'Types file does not include `astro:content` module declaration', 'Types file does not include `astro:content` module declaration',
); );
fixture.thenFileShouldBeValidTypescript('.astro/astro/content.d.ts'); fixture.thenFileShouldBeValidTypescript('.astro/content.d.ts');
}); });
it('Writes types for empty collections', async () => { it('Writes types for empty collections', async () => {
@ -139,7 +139,7 @@ describe('astro sync', () => {
fixture.clean(); fixture.clean();
await fixture.whenSyncing(); await fixture.whenSyncing();
fixture.thenFileContentShouldInclude( fixture.thenFileContentShouldInclude(
'.astro/astro/content.d.ts', '.astro/content.d.ts',
`"blog": Record<string, { `"blog": Record<string, {
id: string; id: string;
slug: string; slug: string;
@ -151,7 +151,7 @@ describe('astro sync', () => {
'Types file does not include empty collection type', 'Types file does not include empty collection type',
); );
fixture.thenFileContentShouldInclude( fixture.thenFileContentShouldInclude(
'.astro/astro/content.d.ts', '.astro/content.d.ts',
`"blogMeta": Record<string, { `"blogMeta": Record<string, {
id: string; id: string;
collection: "blogMeta"; collection: "blogMeta";
@ -170,11 +170,11 @@ describe('astro sync', () => {
fixture.thenFileShouldExist('.astro/types.d.ts'); fixture.thenFileShouldExist('.astro/types.d.ts');
fixture.thenFileContentShouldInclude( fixture.thenFileContentShouldInclude(
'.astro/types.d.ts', '.astro/types.d.ts',
`/// <reference path="astro/env.d.ts" />`, `/// <reference path="env.d.ts" />`,
); );
fixture.thenFileShouldExist('.astro/astro/env.d.ts'); fixture.thenFileShouldExist('.astro/env.d.ts');
fixture.thenFileContentShouldInclude( fixture.thenFileContentShouldInclude(
'.astro/astro/env.d.ts', '.astro/env.d.ts',
`declare module 'astro:env/client' {`, `declare module 'astro:env/client' {`,
'Types file does not include `astro:env` module declaration', 'Types file does not include `astro:env` module declaration',
); );
@ -210,15 +210,15 @@ describe('astro sync', () => {
fixture.thenFileShouldExist('.astro/types.d.ts'); fixture.thenFileShouldExist('.astro/types.d.ts');
fixture.thenFileContentShouldInclude( fixture.thenFileContentShouldInclude(
'.astro/types.d.ts', '.astro/types.d.ts',
`/// <reference path="astro/actions.d.ts" />`, `/// <reference path="actions.d.ts" />`,
); );
fixture.thenFileShouldExist('.astro/astro/actions.d.ts'); fixture.thenFileShouldExist('.astro/actions.d.ts');
fixture.thenFileContentShouldInclude( fixture.thenFileContentShouldInclude(
'.astro/astro/actions.d.ts', '.astro/actions.d.ts',
`declare module "astro:actions" {`, `declare module "astro:actions" {`,
'Types file does not include `astro:actions` module declaration', 'Types file does not include `astro:actions` module declaration',
); );
fixture.thenFileShouldBeValidTypescript('.astro/astro/actions.d.ts'); fixture.thenFileShouldBeValidTypescript('.astro/actions.d.ts');
}); });
}); });
}); });

View file

@ -2,6 +2,7 @@ import * as assert from 'node:assert/strict';
import { describe, it } from 'node:test'; import { describe, it } from 'node:test';
import { validateSupportedFeatures } from '../../../dist/integrations/features-validation.js'; import { validateSupportedFeatures } from '../../../dist/integrations/features-validation.js';
import { import {
normalizeCodegenDir,
normalizeInjectedTypeFilename, normalizeInjectedTypeFilename,
runHookBuildSetup, runHookBuildSetup,
runHookConfigSetup, runHookConfigSetup,
@ -12,6 +13,7 @@ const defaultConfig = {
root: new URL('./', import.meta.url), root: new URL('./', import.meta.url),
srcDir: new URL('src/', import.meta.url), srcDir: new URL('src/', import.meta.url),
}; };
const dotAstroDir = new URL('./.astro/', defaultConfig.root);
describe('Integration API', () => { describe('Integration API', () => {
it('runHookBuildSetup should work', async () => { it('runHookBuildSetup should work', async () => {
@ -87,6 +89,7 @@ describe('Integration API', () => {
}, },
], ],
}, },
dotAstroDir,
}, },
}); });
assert.equal(updatedSettings.config.site, site); assert.equal(updatedSettings.config.site, site);
@ -122,6 +125,7 @@ describe('Integration API', () => {
}, },
], ],
}, },
dotAstroDir,
}, },
}); });
assert.equal(updatedSettings.config.site, site); assert.equal(updatedSettings.config.site, site);
@ -270,3 +274,7 @@ describe('normalizeInjectedTypeFilename', () => {
'./integrations/aA1-_____./types.d.ts', './integrations/aA1-_____./types.d.ts',
); );
}); });
describe('normalizeCodegenDir', () => {
assert.equal(normalizeCodegenDir('aA1-*/_"~.'), './integrations/aA1-_____./');
});