Skip to content

Commit

Permalink
[Content collections] Load content config with full Vite setup (#6092)
Browse files Browse the repository at this point in the history
* feat: use vite dev server for content config

* refactor: improve export naming

* chore: update `sync` to spin up server

* refactor: run sync before build in cli

* fix: move sync call to build setup

* chore: clean up attachContent... types

* chore: remove unneeded comment

* chore: changeset

* fix: attachContentServerListeners in unit tests

* fix: allow forced contentDirExists

* chore: update schema signature

* fix: move content listeners to unit test

* chore remove contentDirExists flag; unused

* chore: stub weird unit test fix
  • Loading branch information
bholmesdev authored Feb 3, 2023
1 parent db2c59f commit bf8d736
Show file tree
Hide file tree
Showing 12 changed files with 286 additions and 236 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-bobcats-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Ensure vite config (aliases, custom modules, etc) is respected when loading the content collection config
21 changes: 19 additions & 2 deletions packages/astro/src/cli/sync/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { dim } from 'kleur/colors';
import type fsMod from 'node:fs';
import { performance } from 'node:perf_hooks';
import { createServer } from 'vite';
import type { AstroSettings } from '../../@types/astro';
import { contentObservable, createContentTypesGenerator } from '../../content/index.js';
import { createContentTypesGenerator } from '../../content/index.js';
import { globalContentConfigObserver } from '../../content/utils.js';
import { getTimeStat } from '../../core/build/util.js';
import { createVite } from '../../core/create-vite.js';
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
import { info, LogOptions } from '../../core/logger/core.js';
import { setUpEnvTs } from '../../vite-plugin-inject-env-ts/index.js';
Expand All @@ -13,13 +16,25 @@ export async function sync(
{ logging, fs }: { logging: LogOptions; fs: typeof fsMod }
): Promise<0 | 1> {
const timerStart = performance.now();
// Needed to load content config
const tempViteServer = await createServer(
await createVite(
{
server: { middlewareMode: true, hmr: false },
optimizeDeps: { entries: [] },
logLevel: 'silent',
},
{ settings, logging, mode: 'build', fs }
)
);

try {
const contentTypesGenerator = await createContentTypesGenerator({
contentConfigObserver: contentObservable({ status: 'loading' }),
contentConfigObserver: globalContentConfigObserver,
logging,
fs,
settings,
viteServer: tempViteServer,
});
const typesResult = await contentTypesGenerator.init();
if (typesResult.typesGenerated === false) {
Expand All @@ -32,6 +47,8 @@ export async function sync(
}
} catch (e) {
throw new AstroError(AstroErrorData.GenerateContentTypesError);
} finally {
await tempViteServer.close();
}

info(logging, 'content', `Types generated ${dim(getTimeStat(timerStart, performance.now()))}`);
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export {
astroContentAssetPropagationPlugin,
astroContentProdBundlePlugin,
} from './vite-plugin-content-assets.js';
export { astroContentServerPlugin } from './vite-plugin-content-server.js';
export { astroContentImportPlugin } from './vite-plugin-content-imports.js';
export { attachContentServerListeners } from './server-listeners.js';
export { astroContentVirtualModPlugin } from './vite-plugin-content-virtual-mod.js';
73 changes: 73 additions & 0 deletions packages/astro/src/content/server-listeners.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { cyan } from 'kleur/colors';
import { pathToFileURL } from 'node:url';
import type fsMod from 'node:fs';
import type { ViteDevServer } from 'vite';
import type { AstroSettings } from '../@types/astro.js';
import { info, LogOptions } from '../core/logger/core.js';
import { appendForwardSlash } from '../core/path.js';
import { createContentTypesGenerator } from './types-generator.js';
import { globalContentConfigObserver, getContentPaths } from './utils.js';

interface ContentServerListenerParams {
fs: typeof fsMod;
logging: LogOptions;
settings: AstroSettings;
viteServer: ViteDevServer;
}

export async function attachContentServerListeners({
viteServer,
fs,
logging,
settings,
}: ContentServerListenerParams) {
const contentPaths = getContentPaths(settings.config);

if (fs.existsSync(contentPaths.contentDir)) {
info(
logging,
'content',
`Watching ${cyan(
contentPaths.contentDir.href.replace(settings.config.root.href, '')
)} for changes`
);
await attachListeners();
} else {
viteServer.watcher.on('addDir', contentDirListener);
async function contentDirListener(dir: string) {
if (appendForwardSlash(pathToFileURL(dir).href) === contentPaths.contentDir.href) {
info(logging, 'content', `Content dir found. Watching for changes`);
await attachListeners();
viteServer.watcher.removeListener('addDir', contentDirListener);
}
}
}

async function attachListeners() {
const contentGenerator = await createContentTypesGenerator({
fs,
settings,
logging,
viteServer,
contentConfigObserver: globalContentConfigObserver,
});
await contentGenerator.init();
info(logging, 'content', 'Types generated');

viteServer.watcher.on('add', (entry) => {
contentGenerator.queueEvent({ name: 'add', entry });
});
viteServer.watcher.on('addDir', (entry) =>
contentGenerator.queueEvent({ name: 'addDir', entry })
);
viteServer.watcher.on('change', (entry) =>
contentGenerator.queueEvent({ name: 'change', entry })
);
viteServer.watcher.on('unlink', (entry) => {
contentGenerator.queueEvent({ name: 'unlink', entry });
});
viteServer.watcher.on('unlinkDir', (entry) =>
contentGenerator.queueEvent({ name: 'unlinkDir', entry })
);
}
}
7 changes: 5 additions & 2 deletions packages/astro/src/content/types-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { cyan } from 'kleur/colors';
import type fsMod from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { normalizePath } from 'vite';
import { normalizePath, ViteDevServer } 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';
Expand Down Expand Up @@ -32,6 +32,8 @@ type CreateContentGeneratorParams = {
contentConfigObserver: ContentObservable;
logging: LogOptions;
settings: AstroSettings;
/** This is required for loading the content config */
viteServer: ViteDevServer;
fs: typeof fsMod;
};

Expand All @@ -44,6 +46,7 @@ export async function createContentTypesGenerator({
fs,
logging,
settings,
viteServer,
}: CreateContentGeneratorParams) {
const contentTypes: ContentTypes = {};
const contentPaths = getContentPaths(settings.config);
Expand Down Expand Up @@ -113,7 +116,7 @@ export async function createContentTypesGenerator({
}
if (fileType === 'config') {
contentConfigObserver.set({ status: 'loading' });
const config = await loadContentConfig({ fs, settings });
const config = await loadContentConfig({ fs, settings, viteServer });
if (config) {
contentConfigObserver.set({ status: 'loaded', config });
} else {
Expand Down
23 changes: 11 additions & 12 deletions packages/astro/src/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,34 +205,32 @@ export function parseFrontmatter(fileContents: string, filePath: string) {
}
}

/**
* The content config is loaded separately from other `src/` files.
* This global observable lets dependent plugins (like the content flag plugin)
* subscribe to changes during dev server updates.
*/
export const globalContentConfigObserver = contentObservable({ status: 'init' });

export async function loadContentConfig({
fs,
settings,
viteServer,
}: {
fs: typeof fsMod;
settings: AstroSettings;
viteServer: ViteDevServer;
}): Promise<ContentConfig | undefined> {
const contentPaths = getContentPaths(settings.config);
const tempConfigServer: ViteDevServer = await createServer({
root: fileURLToPath(settings.config.root),
server: { middlewareMode: true, hmr: false },
optimizeDeps: { entries: [] },
clearScreen: false,
appType: 'custom',
logLevel: 'silent',
plugins: [astroContentVirtualModPlugin({ settings })],
});
let unparsedConfig;
if (!fs.existsSync(contentPaths.config)) {
return undefined;
}
try {
const configPathname = fileURLToPath(contentPaths.config);
unparsedConfig = await tempConfigServer.ssrLoadModule(configPathname);
unparsedConfig = await viteServer.ssrLoadModule(configPathname);
} catch (e) {
throw e;
} finally {
await tempConfigServer.close();
}
const config = contentConfigParser.safeParse(unparsedConfig);
if (config.success) {
Expand All @@ -243,6 +241,7 @@ export async function loadContentConfig({
}

type ContentCtx =
| { status: 'init' }
| { status: 'loading' }
| { status: 'error' }
| { status: 'loaded'; config: ContentConfig };
Expand Down
129 changes: 129 additions & 0 deletions packages/astro/src/content/vite-plugin-content-imports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as devalue from 'devalue';
import { pathToFileURL } from 'url';
import type { Plugin } from 'vite';
import type fsMod from 'node:fs';
import { AstroSettings } from '../@types/astro.js';
import { contentFileExts, CONTENT_FLAG } from './consts.js';
import {
ContentConfig,
globalContentConfigObserver,
getContentPaths,
getEntryData,
getEntryInfo,
getEntrySlug,
parseFrontmatter,
} from './utils.js';
import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js';
import { getEntryType } from './types-generator.js';
import { AstroError } from '../core/errors/errors.js';
import { AstroErrorData } from '../core/errors/errors-data.js';

function isContentFlagImport(viteId: string) {
const { pathname, searchParams } = new URL(viteId, 'file://');
return searchParams.has(CONTENT_FLAG) && contentFileExts.some((ext) => pathname.endsWith(ext));
}

export function astroContentImportPlugin({
fs,
settings,
}: {
fs: typeof fsMod;
settings: AstroSettings;
}): Plugin {
const contentPaths = getContentPaths(settings.config);

return {
name: 'astro:content-imports',
async load(id) {
const { fileId } = getFileInfo(id, settings.config);
if (isContentFlagImport(id)) {
const observable = globalContentConfigObserver.get();

// Content config should be loaded before this plugin is used
if (observable.status === 'init') {
throw new AstroError({
...AstroErrorData.UnknownContentCollectionError,
message: 'Content config failed to load.',
});
}

let contentConfig: ContentConfig | undefined =
observable.status === 'loaded' ? observable.config : undefined;
if (observable.status === 'loading') {
// Wait for config to load
contentConfig = await new Promise((resolve) => {
const unsubscribe = globalContentConfigObserver.subscribe((ctx) => {
if (ctx.status === 'loaded') {
resolve(ctx.config);
unsubscribe();
} else if (ctx.status === 'error') {
resolve(undefined);
unsubscribe();
}
});
});
}
const rawContents = await fs.promises.readFile(fileId, 'utf-8');
const {
content: body,
data: unparsedData,
matter: rawData = '',
} = parseFrontmatter(rawContents, fileId);
const entryInfo = getEntryInfo({
entry: pathToFileURL(fileId),
contentDir: contentPaths.contentDir,
});
if (entryInfo instanceof Error) return;

const _internal = { filePath: fileId, rawData };
const partialEntry = { data: unparsedData, body, _internal, ...entryInfo };
// TODO: move slug calculation to the start of the build
// to generate a performant lookup map for `getEntryBySlug`
const slug = getEntrySlug(partialEntry);

const collectionConfig = contentConfig?.collections[entryInfo.collection];
const data = collectionConfig
? await getEntryData(partialEntry, collectionConfig)
: unparsedData;

const code = escapeViteEnvReferences(`
export const id = ${JSON.stringify(entryInfo.id)};
export const collection = ${JSON.stringify(entryInfo.collection)};
export const slug = ${JSON.stringify(slug)};
export const body = ${JSON.stringify(body)};
export const data = ${devalue.uneval(data) /* TODO: reuse astro props serializer */};
export const _internal = {
filePath: ${JSON.stringify(fileId)},
rawData: ${JSON.stringify(rawData)},
};
`);
return { code };
}
},
configureServer(viteServer) {
viteServer.watcher.on('all', async (event, entry) => {
if (
['add', 'unlink', 'change'].includes(event) &&
getEntryType(entry, contentPaths) === 'config'
) {
// Content modules depend on config, so we need to invalidate them.
for (const modUrl of viteServer.moduleGraph.urlToModuleMap.keys()) {
if (isContentFlagImport(modUrl)) {
const mod = await viteServer.moduleGraph.getModuleByUrl(modUrl);
if (mod) {
viteServer.moduleGraph.invalidateModule(mod);
}
}
}
}
});
},
async transform(code, id) {
if (isContentFlagImport(id)) {
// Escape before Rollup internal transform.
// Base on MUCH trial-and-error, inspired by MDX integration 2-step transform.
return { code: escapeViteEnvReferences(code) };
}
},
};
}
Loading

0 comments on commit bf8d736

Please sign in to comment.