Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Content] Add astro sync type gen command #5647

Merged
merged 5 commits into from
Dec 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/good-suns-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': minor
---

Add `astro sync` CLI command for type generation
10 changes: 10 additions & 0 deletions packages/astro/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type CLICommand =
| 'build'
| 'preview'
| 'reload'
| 'sync'
| 'check'
| 'telemetry';

Expand All @@ -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': [
Expand All @@ -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';
Expand Down Expand Up @@ -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');

Expand Down
31 changes: 31 additions & 0 deletions packages/astro/src/cli/sync/index.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions packages/astro/src/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,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';
15 changes: 10 additions & 5 deletions packages/astro/src/content/types-generator.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
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';
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 };
Expand All @@ -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;
Expand All @@ -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'
);
Expand Down
1 change: 0 additions & 1 deletion packages/astro/src/content/vite-plugin-content-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export function astroContentServerPlugin({
settings,
logging,
contentConfigObserver,
contentPaths,
});
await contentGenerator.init();
info(logging, 'content', 'Types generated');
Expand Down
24 changes: 24 additions & 0 deletions packages/astro/src/core/errors/errors-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
35 changes: 33 additions & 2 deletions packages/astro/test/content-collections.test.js
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
types.generated.d.ts

This file was deleted.

2 changes: 2 additions & 0 deletions packages/astro/test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down