summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/good-suns-mate.md5
-rw-r--r--packages/astro/src/cli/index.ts10
-rw-r--r--packages/astro/src/cli/sync/index.ts31
-rw-r--r--packages/astro/src/content/index.ts2
-rw-r--r--packages/astro/src/content/types-generator.ts15
-rw-r--r--packages/astro/src/content/vite-plugin-content-server.ts1
-rw-r--r--packages/astro/src/core/errors/errors-data.ts24
-rw-r--r--packages/astro/test/content-collections.test.js35
-rw-r--r--packages/astro/test/fixtures/content-collections/.gitignore1
-rw-r--r--packages/astro/test/fixtures/content-collections/src/content/types.generated.d.ts125
-rw-r--r--packages/astro/test/test-utils.js2
11 files changed, 118 insertions, 133 deletions
diff --git a/.changeset/good-suns-mate.md b/.changeset/good-suns-mate.md
new file mode 100644
index 000000000..20ff5f251
--- /dev/null
+++ b/.changeset/good-suns-mate.md
@@ -0,0 +1,5 @@
+---
+'astro': minor
+---
+
+Add `astro sync` CLI command for type generation
diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts
index 0a0d84b45..02a9075ec 100644
--- a/packages/astro/src/cli/index.ts
+++ b/packages/astro/src/cli/index.ts
@@ -31,6 +31,7 @@ type CLICommand =
| 'build'
| 'preview'
| 'reload'
+ | 'sync'
| 'check'
| 'telemetry';
@@ -48,6 +49,7 @@ function printAstroHelp() {
['dev', 'Start the development server.'],
['docs', 'Open documentation in your web browser.'],
['preview', 'Preview your build locally.'],
+ ['sync', 'Generate content collection types.'],
['telemetry', 'Configure telemetry settings.'],
],
'Global Flags': [
@@ -74,6 +76,7 @@ async function printVersion() {
function resolveCommand(flags: Arguments): CLICommand {
const cmd = flags._[2] as string;
if (cmd === 'add') return 'add';
+ if (cmd === 'sync') return 'sync';
if (cmd === 'telemetry') return 'telemetry';
if (flags.version) return 'version';
else if (flags.help) return 'help';
@@ -202,6 +205,13 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
return process.exit(ret);
}
+ case 'sync': {
+ const { sync } = await import('./sync/index.js');
+
+ const ret = await sync(settings, { logging, fs });
+ return process.exit(ret);
+ }
+
case 'preview': {
const { default: preview } = await import('../core/preview/index.js');
diff --git a/packages/astro/src/cli/sync/index.ts b/packages/astro/src/cli/sync/index.ts
new file mode 100644
index 000000000..0e63fb02e
--- /dev/null
+++ b/packages/astro/src/cli/sync/index.ts
@@ -0,0 +1,31 @@
+import type fsMod from 'node:fs';
+import { performance } from 'node:perf_hooks';
+import { dim } from 'kleur/colors';
+import type { AstroSettings } from '../../@types/astro';
+import { info, LogOptions } from '../../core/logger/core.js';
+import { contentObservable, createContentTypesGenerator } from '../../content/index.js';
+import { getTimeStat } from '../../core/build/util.js';
+import { AstroError, AstroErrorData } from '../../core/errors/index.js';
+
+export async function sync(
+ settings: AstroSettings,
+ { logging, fs }: { logging: LogOptions; fs: typeof fsMod }
+): Promise<0 | 1> {
+ const timerStart = performance.now();
+
+ try {
+ const contentTypesGenerator = await createContentTypesGenerator({
+ contentConfigObserver: contentObservable({ status: 'loading' }),
+ logging,
+ fs,
+ settings,
+ });
+ await contentTypesGenerator.init();
+ } catch (e) {
+ throw new AstroError(AstroErrorData.GenerateContentTypesError);
+ }
+
+ info(logging, 'content', `Types generated ${dim(getTimeStat(timerStart, performance.now()))}`);
+
+ return 0;
+}
diff --git a/packages/astro/src/content/index.ts b/packages/astro/src/content/index.ts
index 15b61b5f2..ccb36982d 100644
--- a/packages/astro/src/content/index.ts
+++ b/packages/astro/src/content/index.ts
@@ -5,3 +5,5 @@ export {
} from './vite-plugin-content-assets.js';
export { astroContentServerPlugin } from './vite-plugin-content-server.js';
export { astroContentVirtualModPlugin } from './vite-plugin-content-virtual-mod.js';
+export { contentObservable } from './utils.js';
+export { createContentTypesGenerator } from './types-generator.js';
diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts
index 5162cbcd5..9b9f925b8 100644
--- a/packages/astro/src/content/types-generator.ts
+++ b/packages/astro/src/content/types-generator.ts
@@ -1,6 +1,6 @@
import glob from 'fast-glob';
import { cyan } from 'kleur/colors';
-import fsMod from 'node:fs';
+import type fsMod from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { normalizePath } from 'vite';
@@ -8,7 +8,13 @@ import type { AstroSettings } from '../@types/astro.js';
import { info, LogOptions, warn } from '../core/logger/core.js';
import { appendForwardSlash, isRelativePath } from '../core/path.js';
import { contentFileExts, CONTENT_TYPES_FILE } from './consts.js';
-import { ContentConfig, ContentObservable, ContentPaths, loadContentConfig } from './utils.js';
+import {
+ ContentConfig,
+ ContentObservable,
+ ContentPaths,
+ getContentPaths,
+ loadContentConfig,
+} from './utils.js';
type ChokidarEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
type RawContentEvent = { name: ChokidarEvent; entry: string };
@@ -28,7 +34,6 @@ type ContentTypesEntryMetadata = { slug: string };
type ContentTypes = Record<string, Record<string, ContentTypesEntryMetadata>>;
type CreateContentGeneratorParams = {
- contentPaths: ContentPaths;
contentConfigObserver: ContentObservable;
logging: LogOptions;
settings: AstroSettings;
@@ -40,18 +45,18 @@ type EventOpts = { logLevel: 'info' | 'warn' };
class UnsupportedFileTypeError extends Error {}
export async function createContentTypesGenerator({
- contentPaths,
contentConfigObserver,
fs,
logging,
settings,
}: CreateContentGeneratorParams): Promise<GenerateContentTypes> {
const contentTypes: ContentTypes = {};
+ const contentPaths: ContentPaths = getContentPaths({ srcDir: settings.config.srcDir });
let events: Promise<{ shouldGenerateTypes: boolean; error?: Error }>[] = [];
let debounceTimeout: NodeJS.Timeout | undefined;
- const contentTypesBase = await fsMod.promises.readFile(
+ const contentTypesBase = await fs.promises.readFile(
new URL(CONTENT_TYPES_FILE, contentPaths.generatedInputDir),
'utf-8'
);
diff --git a/packages/astro/src/content/vite-plugin-content-server.ts b/packages/astro/src/content/vite-plugin-content-server.ts
index 5df38b742..a56df3b42 100644
--- a/packages/astro/src/content/vite-plugin-content-server.ts
+++ b/packages/astro/src/content/vite-plugin-content-server.ts
@@ -60,7 +60,6 @@ export function astroContentServerPlugin({
settings,
logging,
contentConfigObserver,
- contentPaths,
});
await contentGenerator.init();
info(logging, 'content', 'Types generated');
diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts
index cc6e164c5..57cf9f4d6 100644
--- a/packages/astro/src/core/errors/errors-data.ts
+++ b/packages/astro/src/core/errors/errors-data.ts
@@ -552,6 +552,30 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati
message: (legacyConfigKey: string) => `Legacy configuration detected: \`${legacyConfigKey}\`.`,
hint: 'Please update your configuration to the new format.\nSee https://astro.build/config for more information.',
},
+ /**
+ * @docs
+ * @kind heading
+ * @name CLI Errors
+ */
+ // CLI Errors - 8xxx
+ UnknownCLIError: {
+ title: 'Unknown CLI Error.',
+ code: 8000,
+ },
+ /**
+ * @docs
+ * @description
+ * `astro sync` command failed to generate content collection types.
+ * @see
+ * - [Content collections documentation](https://docs.astro.build/en/guides/content-collections/)
+ */
+ GenerateContentTypesError: {
+ title: 'Failed to generate content types.',
+ code: 8001,
+ message: '`astro sync` command failed to generate content collection types.',
+ hint: 'Check your `src/content/config.*` file for typos.',
+ },
+
// Generic catch-all
UnknownError: {
title: 'Unknown Error.',
diff --git a/packages/astro/test/content-collections.test.js b/packages/astro/test/content-collections.test.js
index d52521959..a66287080 100644
--- a/packages/astro/test/content-collections.test.js
+++ b/packages/astro/test/content-collections.test.js
@@ -1,10 +1,41 @@
+import * as fs from 'node:fs';
+import * as devalue from 'devalue';
+import * as cheerio from 'cheerio';
import { expect } from 'chai';
import { loadFixture } from './test-utils.js';
import testAdapter from './test-adapter.js';
-import * as devalue from 'devalue';
-import * as cheerio from 'cheerio';
describe('Content Collections', () => {
+ describe('Type generation', () => {
+ let fixture;
+ before(async () => {
+ fixture = await loadFixture({ root: './fixtures/content-collections/' });
+ });
+
+ it('Writes types to `src/content/`', async () => {
+ let writtenFiles = {};
+ const fsMock = {
+ ...fs,
+ promises: {
+ ...fs.promises,
+ async writeFile(path, contents) {
+ writtenFiles[path] = contents;
+ },
+ },
+ };
+ const expectedTypesFile = new URL('./content/types.generated.d.ts', fixture.config.srcDir)
+ .href;
+ await fixture.sync({ fs: fsMock });
+ expect(Object.keys(writtenFiles)).to.have.lengthOf(1);
+ expect(writtenFiles).to.haveOwnProperty(expectedTypesFile);
+ // smoke test `astro check` asserts whether content types pass.
+ expect(writtenFiles[expectedTypesFile]).to.include(
+ `declare module 'astro:content' {`,
+ 'Types file does not include `astro:content` module declaration'
+ );
+ });
+ });
+
describe('Query', () => {
let fixture;
before(async () => {
diff --git a/packages/astro/test/fixtures/content-collections/.gitignore b/packages/astro/test/fixtures/content-collections/.gitignore
new file mode 100644
index 000000000..54f79dcd6
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/.gitignore
@@ -0,0 +1 @@
+types.generated.d.ts
diff --git a/packages/astro/test/fixtures/content-collections/src/content/types.generated.d.ts b/packages/astro/test/fixtures/content-collections/src/content/types.generated.d.ts
deleted file mode 100644
index bf03efb76..000000000
--- a/packages/astro/test/fixtures/content-collections/src/content/types.generated.d.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-declare module 'astro:content' {
- export { z } from 'astro/zod';
- export type CollectionEntry<C extends keyof typeof entryMap> =
- typeof entryMap[C][keyof typeof entryMap[C]] & Render;
-
- type BaseCollectionConfig<S extends import('astro/zod').ZodRawShape> = {
- schema?: S;
- slug?: (entry: {
- id: CollectionEntry<keyof typeof entryMap>['id'];
- defaultSlug: string;
- collection: string;
- body: string;
- data: import('astro/zod').infer<import('astro/zod').ZodObject<S>>;
- }) => string | Promise<string>;
- };
- export function defineCollection<S extends import('astro/zod').ZodRawShape>(
- input: BaseCollectionConfig<S>
- ): BaseCollectionConfig<S>;
-
- export function getEntry<C extends keyof typeof entryMap, E extends keyof typeof entryMap[C]>(
- collection: C,
- entryKey: E
- ): Promise<typeof entryMap[C][E] & Render>;
- export function getCollection<
- C extends keyof typeof entryMap,
- E extends keyof typeof entryMap[C]
- >(
- collection: C,
- filter?: (data: typeof entryMap[C][E]) => boolean
- ): Promise<(typeof entryMap[C][E] & Render)[]>;
-
- type InferEntrySchema<C extends keyof typeof entryMap> = import('astro/zod').infer<
- import('astro/zod').ZodObject<Required<ContentConfig['collections'][C]>['schema']>
- >;
-
- type Render = {
- render(): Promise<{
- Content: import('astro').MarkdownInstance<{}>['Content'];
- headings: import('astro').MarkdownHeading[];
- injectedFrontmatter: Record<string, any>;
- }>;
- };
-
- const entryMap: {
- "with-schema-config": {
-"one.md": {
- id: "one.md",
- slug: "one",
- body: string,
- collection: "with-schema-config",
- data: InferEntrySchema<"with-schema-config">
-},
-"three.md": {
- id: "three.md",
- slug: "three",
- body: string,
- collection: "with-schema-config",
- data: InferEntrySchema<"with-schema-config">
-},
-"two.md": {
- id: "two.md",
- slug: "two",
- body: string,
- collection: "with-schema-config",
- data: InferEntrySchema<"with-schema-config">
-},
-},
-"with-slug-config": {
-"one.md": {
- id: "one.md",
- slug: string,
- body: string,
- collection: "with-slug-config",
- data: InferEntrySchema<"with-slug-config">
-},
-"three.md": {
- id: "three.md",
- slug: string,
- body: string,
- collection: "with-slug-config",
- data: InferEntrySchema<"with-slug-config">
-},
-"two.md": {
- id: "two.md",
- slug: string,
- body: string,
- collection: "with-slug-config",
- data: InferEntrySchema<"with-slug-config">
-},
-},
-"without-config": {
-"columbia.md": {
- id: "columbia.md",
- slug: "columbia",
- body: string,
- collection: "without-config",
- data: any
-},
-"endeavour.md": {
- id: "endeavour.md",
- slug: "endeavour",
- body: string,
- collection: "without-config",
- data: any
-},
-"enterprise.md": {
- id: "enterprise.md",
- slug: "enterprise",
- body: string,
- collection: "without-config",
- data: any
-},
-"promo/launch-week.mdx": {
- id: "promo/launch-week.mdx",
- slug: "promo/launch-week",
- body: string,
- collection: "without-config",
- data: any
-},
-},
-
- };
-
- type ContentConfig = typeof import("./config");
-}
diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js
index 3c14a9194..3e370e647 100644
--- a/packages/astro/test/test-utils.js
+++ b/packages/astro/test/test-utils.js
@@ -7,6 +7,7 @@ import { createSettings } from '../dist/core/config/index.js';
import dev from '../dist/core/dev/index.js';
import build from '../dist/core/build/index.js';
import preview from '../dist/core/preview/index.js';
+import { sync } from '../dist/cli/sync/index.js';
import { nodeLogDestination } from '../dist/core/logger/node.js';
import os from 'os';
import stripAnsi from 'strip-ansi';
@@ -139,6 +140,7 @@ export async function loadFixture(inlineConfig) {
return {
build: (opts = {}) => build(settings, { logging, telemetry, ...opts }),
+ sync: (opts) => sync(settings, { logging, fs, ...opts }),
startDevServer: async (opts = {}) => {
devServer = await dev(settings, { logging, telemetry, ...opts });
config.server.host = parseAddressToHost(devServer.address.address); // update host