feat: project overrides

This commit is contained in:
2024-11-29 02:26:13 +02:00
parent 97f8c63daf
commit e0ed6cda76
5 changed files with 56 additions and 18 deletions

View File

@@ -3,6 +3,8 @@
This is a content loader for Astro that fetches GitHub repositories and their README files, so you
can list them easily in your Astro site.
[See a demo on my website](https://casraf.dev/projects)
## Usage
In your `src/content/config.ts` file, add a new collection and use the loader:
@@ -19,6 +21,7 @@ const project = defineCollection({
orgs: ['myorg'], // A list of GitHub orgs to fetch repositories from
debug: true, // Output debug logs during processing
force: false, // Ignore cache and force a full re-fetch
overridesDir: 'src/content/project-overrides', // Directory to look for overrides
filter: (repo) => // Filter repositories to include in the collection
[
!repo.fork,
@@ -46,6 +49,26 @@ const projects = await getCollection('project')
</div>
```
### Overrides
You might want to add, or modify data for a project. To keep this easy to maintain, and to allow
markdown parsing, you can create a `my_project.md` file inside your `overridesDir`.
The default directory is `src/content/project-overrides`.
Just create an md file with all the related fields you want to override. Insert the readme Markdown
content under the properties.
```md
---
# src/content/project-overrides/my_project.md
title: My Project # instead of my_project
featured: true
---
This is the README content for my project. Hooray!
```
## Contributing
I am developing this package on my free time, so any support, whether code, issues, or just stars is

View File

@@ -3,9 +3,12 @@ import path from 'node:path'
import { logger } from './logger.js'
import { GitHubRepositoryAPIResponse, InternalLoaderOptions } from './types.js'
export const overridesDir = path.join(process.cwd(), 'src', 'content', 'project-overrides')
export const projectIgnoreFile = path.join(overridesDir, '.projectignore')
export const projectKeepFile = path.join(overridesDir, '.projectkeep')
export function getProjectIgnoreFile(options: Pick<InternalLoaderOptions, 'overridesDir'>) {
return path.resolve(options.overridesDir, '.projectignore')
}
export function getProjectKeepFile(options: Pick<InternalLoaderOptions, 'overridesDir'>) {
return path.resolve(options.overridesDir, '.projectkeep')
}
export let projectIgnore: string[] = []
export let projectKeep: string[] = []
@@ -58,9 +61,9 @@ async function loadFileList(file: string): Promise<string[]> {
.filter((x) => x[0] !== '#')
}
export async function reloadOverrides() {
projectIgnore = await loadFileList(projectIgnoreFile)
projectKeep = await loadFileList(projectKeepFile)
export async function reloadOverrides(options: InternalLoaderOptions) {
projectIgnore = await loadFileList(getProjectIgnoreFile(options))
projectKeep = await loadFileList(getProjectKeepFile(options))
}
export function getAuthorization(options: InternalLoaderOptions): Headers {

View File

@@ -1,10 +1,13 @@
import type { Loader, LoaderContext } from 'astro/loaders'
import { projectIgnoreFile, projectKeepFile, reloadOverrides } from './github.js'
import { getProjectIgnoreFile, getProjectKeepFile, reloadOverrides } from './github.js'
import { parseJSON as parseDate } from 'date-fns/parseJSON'
import { formatISO as formatDate } from 'date-fns/formatISO'
import { LoaderOptions, InternalLoaderOptions } from './types.js'
import { logger } from './logger.js'
import { getProjectsList } from './parser.js'
import path from 'path'
const defaultOverridesDir = path.join(process.cwd(), 'src', 'content', 'project-overrides')
async function reloadProjects(
{ store, meta }: Pick<LoaderContext, 'store' | 'meta'>,
@@ -15,13 +18,14 @@ async function reloadProjects(
(opts.force ? undefined : meta.get('lastUpdated')) ?? '1970-01-01T00:00:00Z',
)
await reloadOverrides()
const options = InternalLoaderOptions.parse({
debug: false,
orgs: [],
overridesDir: defaultOverridesDir,
...opts,
lastUpdated,
})
await reloadOverrides(options)
const projects = await getProjectsList(options)
logger.log(projects.length, 'projects loaded')
@@ -50,7 +54,7 @@ export function githubProjectsLoader(opts: LoaderOptions): Loader {
watcher?.on('change', async (filename) => {
logger.log('Change detected:', filename)
if ([projectIgnoreFile, projectKeepFile].includes(filename)) {
if (path.dirname(filename) === (opts.overridesDir ?? defaultOverridesDir)) {
await reloadProjects({ store, meta }, opts)
}
})

View File

@@ -6,7 +6,7 @@ import { formatISO as formatDate } from 'date-fns/formatISO'
import { GitHubProjectSchema, GitHubRepositoryAPIResponse, InternalLoaderOptions } from './types.js'
import { fileExists, parseMarkdown } from './utils.js'
import { logger } from './logger.js'
import { fetchRepos, getAuthorization, overridesDir, projectIgnore, projectKeep } from './github.js'
import { fetchRepos, getAuthorization, projectIgnore, projectKeep } from './github.js'
export async function getProjectsList(
options: InternalLoaderOptions,
@@ -44,7 +44,7 @@ export async function getProjectsList(
raw: repo,
})
const overridesFile = path.join(overridesDir, `${project.name}.md`)
const overridesFile = path.join(options.overridesDir, `${project.name}.md`)
if (await fileExists(overridesFile)) {
const content = await fs.readFile(overridesFile, 'utf8')

View File

@@ -9,6 +9,10 @@ export const LinkSchema = z.object({
href: z.string(),
icon: z.string(),
})
/**
* Schema for a link.
*/
export type LinkSchema = {
/** The title of the link. */
title: string
@@ -35,6 +39,9 @@ export const GitHubProjectSchema = z.object({
raw: z.any() as z.ZodType<GitHubRepositoryAPIResponse>,
})
/**
* Schema for a GitHub project.
*/
export type GitHubProjectSchema = {
/** The name of the GitHub project. */
name: string
@@ -69,23 +76,22 @@ export type GitHubRepositoryAPIResponse = NonNullable<
* Schema for loader options.
*/
export const LoaderOptions = z.object({
/** The username from GitHub to fetch the repositories from. */
username: z.string(),
/** Debug flag - if true, the loader will output debug information. */
debug: z.boolean().optional(),
/** GitHub organizations to fetch projects from. */
orgs: z.array(z.string()).optional(),
/** A function to filter out projects. Filtered projects will not be parsed or saved in the collection. */
filter: z
.function()
.args(z.any() as z.ZodType<GitHubRepositoryAPIResponse>)
.returns(z.boolean())
.optional(),
/** The GitHub API token to use for fetching the repositories. */
apiToken: z.string(),
/** Whether to force a reload of the projects, regardless of cache status. */
force: z.boolean().optional(),
overridesDir: z.string().optional(),
})
/**
* Schema for loader options.
*/
export type LoaderOptions = {
/** The username from GitHub to fetch the repositories from. */
username: string
@@ -100,6 +106,8 @@ export type LoaderOptions = {
apiToken: string
/** Whether to force a reload of the projects, regardless of cache status. */
force?: boolean
/** The directory where project overrides are stored (default: `src/content/project-overrides`). */
overridesDir?: string
}
/** @internal */
@@ -108,7 +116,7 @@ export const InternalLoaderOptions = LoaderOptions.required().extend({
})
/** @internal */
export type InternalLoaderOptions = LoaderOptions & {
export type InternalLoaderOptions = Required<LoaderOptions> & {
/** The last time the projects were updated. */
lastUpdated: Date
}