Skip to content

Commit

Permalink
feat: src/data/!
Browse files Browse the repository at this point in the history
  • Loading branch information
bholmesdev committed Apr 27, 2023
1 parent 70b0cbc commit cc637ec
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 91 deletions.
2 changes: 1 addition & 1 deletion packages/astro/src/content/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { CONTENT_FLAG, PROPAGATED_ASSET_FLAG } from './consts.js';
export { errorMap } from './error-map.js';
export { attachContentServerListeners } from './server-listeners.js';
export { createContentTypesGenerator } from './types-generator.js';
export { createCollectionTypesGenerator } from './types-generator.js';
export { contentObservable, getContentPaths, getDotAstroTypeReference } from './utils.js';
export { astroContentAssetPropagationPlugin } from './vite-plugin-content-assets.js';
export { astroContentImportPlugin } from './vite-plugin-content-imports.js';
Expand Down
8 changes: 4 additions & 4 deletions packages/astro/src/content/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ type GetEntryImport = (collection: string, lookupId: string) => Promise<LazyImpo

export function createCollectionToGlobResultMap({
globResult,
contentDir,
dir,
}: {
globResult: GlobResult;
contentDir: string;
dir: string;
}) {
const collectionToGlobResultMap: CollectionToEntryMap = {};
for (const key in globResult) {
const keyRelativeToContentDir = key.replace(new RegExp(`^${contentDir}`), '');
const segments = keyRelativeToContentDir.split('/');
const keyRelativeToDir = key.replace(new RegExp(`^${dir}`), '');
const segments = keyRelativeToDir.split('/');
if (segments.length <= 1) continue;
const collection = segments[0];
collectionToGlobResultMap[collection] ??= {};
Expand Down
68 changes: 49 additions & 19 deletions packages/astro/src/content/server-listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ import type { ViteDevServer } from 'vite';
import type { AstroSettings } from '../@types/astro.js';
import { loadTSConfig } from '../core/config/tsconfig.js';
import { info, warn, type LogOptions } from '../core/logger/core.js';
import { appendForwardSlash } from '../core/path.js';
import { createContentTypesGenerator } from './types-generator.js';
import { getContentPaths, globalContentConfigObserver, type ContentPaths } from './utils.js';
import { createCollectionTypesGenerator } from './types-generator.js';
import {
getContentPaths,
globalContentConfigObserver,
type ContentPaths,
getCollectionDirByUrl,
} from './utils.js';
import { rootRelativePath } from '../core/util.js';

interface ContentServerListenerParams {
fs: typeof fsMod;
Expand All @@ -24,54 +29,79 @@ export async function attachContentServerListeners({
settings,
}: ContentServerListenerParams) {
const contentPaths = getContentPaths(settings.config, fs);
const { root } = settings.config;
const existentDirs = [contentPaths.contentDir, contentPaths.dataDir].filter((p) =>
fs.existsSync(p)
);

if (fs.existsSync(contentPaths.contentDir)) {
if (existentDirs.length) {
info(
logging,
'content',
`Watching ${cyan(
contentPaths.contentDir.href.replace(settings.config.root.href, '')
`Watching ${bold(
cyan(existentDirs.map((dir) => rootRelativePath(root, dir, false)).join(' and '))
)} for changes`
);
const maybeTsConfigStats = getTSConfigStatsWhenAllowJsFalse({ contentPaths, settings });
if (maybeTsConfigStats) warnAllowJsIsFalse({ ...maybeTsConfigStats, logging });
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);
viteServer.watcher.on('addDir', dirListeners);
// TODO: clean up a bit
let attached = false;
let foundContentDir = false;
let foundDataDir = false;
async function dirListeners(dir: string) {
const collectionDir = getCollectionDirByUrl(pathToFileURL(dir), contentPaths);
if (collectionDir === 'content') foundContentDir = true;
if (collectionDir === 'data') foundDataDir = true;
if (collectionDir) {
info(
logging,
'content',
`Watching ${cyan(
rootRelativePath(
collectionDir === 'content' ? contentPaths.contentDir : contentPaths.dataDir,
root
)
)} for changes`
);
if (!attached) {
await attachListeners();
attached = true;
}
if (foundContentDir && foundDataDir) {
viteServer.watcher.removeListener('addDir', dirListeners);
}
}
}
}

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

viteServer.watcher.on('add', (entry) => {
contentGenerator.queueEvent({ name: 'add', entry });
typesGenerator.queueEvent({ name: 'add', entry });
});
viteServer.watcher.on('addDir', (entry) =>
contentGenerator.queueEvent({ name: 'addDir', entry })
typesGenerator.queueEvent({ name: 'addDir', entry })
);
viteServer.watcher.on('change', (entry) =>
contentGenerator.queueEvent({ name: 'change', entry })
typesGenerator.queueEvent({ name: 'change', entry })
);
viteServer.watcher.on('unlink', (entry) => {
contentGenerator.queueEvent({ name: 'unlink', entry });
typesGenerator.queueEvent({ name: 'unlink', entry });
});
viteServer.watcher.on('unlinkDir', (entry) =>
contentGenerator.queueEvent({ name: 'unlinkDir', entry })
typesGenerator.queueEvent({ name: 'unlinkDir', entry })
);
}
}
Expand Down
7 changes: 4 additions & 3 deletions packages/astro/src/content/template/virtual-mod.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,22 @@ export const image = () => {
};

const contentDir = '@@CONTENT_DIR@@';
const dataDir = '@@DATA_DIR@@';

const contentEntryGlob = import.meta.glob('@@CONTENT_ENTRY_GLOB_PATH@@', {
query: { astroContentCollectionEntry: true },
});
const contentCollectionToEntryMap = createCollectionToGlobResultMap({
globResult: contentEntryGlob,
contentDir,
dir: contentDir,
});

const dataEntryGlob = import.meta.glob('@@DATA_ENTRY_GLOB_PATH@@', {
query: { astroDataCollectionEntry: true },
});
const dataCollectionToEntryMap = createCollectionToGlobResultMap({
globResult: dataEntryGlob,
contentDir,
dir: dataDir,
});

let lookupMap = {};
Expand All @@ -60,7 +61,7 @@ const renderEntryGlob = import.meta.glob('@@RENDER_ENTRY_GLOB_PATH@@', {
});
const collectionToRenderEntryMap = createCollectionToGlobResultMap({
globResult: renderEntryGlob,
contentDir,
dir: contentDir,
});

export const getCollection = createGetCollection({
Expand Down
70 changes: 42 additions & 28 deletions packages/astro/src/content/types-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import {
getEntryCollectionName,
getDataEntryExts,
getDataEntryId,
getCollectionDirByUrl,
} from './utils.js';
import { rootRelativePath } from '../core/util.js';

type ChokidarEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
type RawContentEvent = { name: ChokidarEvent; entry: string };
type ContentEvent = { name: ChokidarEvent; entry: URL };
type ContentEvent = { name: ChokidarEvent; entry: URL; collectionDir: 'content' | 'data' };

type ContentEntryMetadata = { type: 'content'; slug: string };
type DataEntryMetadata = { type: 'data' };
Expand All @@ -45,13 +47,13 @@ type CreateContentGeneratorParams = {
type EventOpts = { logLevel: 'info' | 'warn' };

type EventWithOptions = {
type: ContentEvent;
event: ContentEvent;
opts: EventOpts | undefined;
};

class UnsupportedFileTypeError extends Error {}

export async function createContentTypesGenerator({
export async function createCollectionTypesGenerator({
contentConfigObserver,
fs,
logging,
Expand All @@ -70,32 +72,39 @@ export async function createContentTypesGenerator({
const typeTemplateContent = await fs.promises.readFile(contentPaths.typesTemplate, 'utf-8');

async function init(): Promise<
{ typesGenerated: true } | { typesGenerated: false; reason: 'no-content-dir' }
{ typesGenerated: true } | { typesGenerated: false; reason: 'no-dirs' }
> {
if (!fs.existsSync(contentPaths.contentDir)) {
return { typesGenerated: false, reason: 'no-content-dir' };
if (!fs.existsSync(contentPaths.contentDir) && !fs.existsSync(contentPaths.dataDir)) {
return { typesGenerated: false, reason: 'no-dirs' };
}

events.push({
type: { name: 'add', entry: contentPaths.config.url },
event: { name: 'add', entry: contentPaths.config.url, collectionDir: 'content' },
opts: { logLevel: 'warn' },
});

const globResult = await glob('**', {
cwd: fileURLToPath(contentPaths.contentDir),
const relContentDir = rootRelativePath(settings.config.root, contentPaths.contentDir, false);
const relDataDir = rootRelativePath(settings.config.root, contentPaths.dataDir, false);

const globResult = await glob([relContentDir + '**', relDataDir + '**'], {
absolute: true,
cwd: fileURLToPath(settings.config.root),
fs: {
readdir: fs.readdir.bind(fs),
readdirSync: fs.readdirSync.bind(fs),
},
});
const entries = globResult
.map((e) => new URL(e, contentPaths.contentDir))
.map((e) => pathToFileURL(e))
.filter(
// Config loading handled first. Avoid running twice.
(e) => !e.href.startsWith(contentPaths.config.url.href)
);
for (const entry of entries) {
events.push({ type: { name: 'add', entry }, opts: { logLevel: 'warn' } });
events.push({
event: { name: 'add', entry, collectionDir: getCollectionDirByUrl(entry, contentPaths)! },
opts: { logLevel: 'warn' },
});
}
await runEvents();
return { typesGenerated: true };
Expand Down Expand Up @@ -132,6 +141,7 @@ export async function createContentTypesGenerator({
contentPaths,
contentEntryExts,
dataEntryExts,
event.collectionDir,
settings.config.experimental.assets
);
if (fileType === 'ignored') {
Expand Down Expand Up @@ -173,26 +183,28 @@ export async function createContentTypesGenerator({
}

const { entry } = event;
const { contentDir } = contentPaths;
const collectionDir =
event.collectionDir === 'content' ? contentPaths.contentDir : contentPaths.dataDir;

const collection = getEntryCollectionName({ entry, contentDir });
const collection = getEntryCollectionName({
entry,
dir: collectionDir,
});
if (collection === undefined) {
if (['info', 'warn'].includes(logLevel)) {
warn(
logging,
'content',
`${cyan(
normalizePath(
path.relative(fileURLToPath(contentPaths.contentDir), fileURLToPath(event.entry))
)
normalizePath(path.relative(fileURLToPath(collectionDir), fileURLToPath(event.entry)))
)} must be nested in a collection directory. Skipping.`
);
}
return { shouldGenerateTypes: false };
}

if (fileType === 'data') {
const id = getDataEntryId({ entry, contentDir, collection });
const id = getDataEntryId({ entry, dataDir: contentPaths.dataDir, collection });
const collectionKey = JSON.stringify(collection);
const entryKey = JSON.stringify(id);

Expand All @@ -217,7 +229,11 @@ export async function createContentTypesGenerator({

const contentEntryType = contentEntryConfigByExt.get(path.extname(event.entry.pathname));
if (!contentEntryType) return { shouldGenerateTypes: false };
const { id, slug: generatedSlug } = getContentEntryIdAndSlug({ entry, contentDir, collection });
const { id, slug: generatedSlug } = getContentEntryIdAndSlug({
entry,
contentDir: contentPaths.contentDir,
collection,
});

const collectionKey = JSON.stringify(collection);
const entryKey = JSON.stringify(id);
Expand Down Expand Up @@ -268,16 +284,14 @@ export async function createContentTypesGenerator({
}

function queueEvent(rawEvent: RawContentEvent, opts?: EventOpts) {
const event = {
type: {
entry: pathToFileURL(rawEvent.entry),
name: rawEvent.name,
},
opts,
};
if (!event.type.entry.pathname.startsWith(contentPaths.contentDir.pathname)) return;
const entryUrl = pathToFileURL(rawEvent.entry);
const collectionDir = getCollectionDirByUrl(entryUrl, contentPaths);
if (!collectionDir) return;

events.push(event);
events.push({
event: { name: rawEvent.name, entry: entryUrl, collectionDir: collectionDir },
opts,
});

debounceTimeout && clearTimeout(debounceTimeout);
const runEventsSafe = async () => {
Expand All @@ -297,7 +311,7 @@ export async function createContentTypesGenerator({
const eventResponses = [];

for (const event of events) {
const response = await handleEvent(event.type, event.opts);
const response = await handleEvent(event.event, event.opts);
eventResponses.push(response);
}

Expand Down
Loading

0 comments on commit cc637ec

Please sign in to comment.