Tucker McKnight <tucker@pangolin.lan> | Sun Jan 11 2026
Change the before hook to an exported function Also pass in the site output directory to the getLocation function, since all calls to git now check in the cloned copy of the repo in the _site dir (or whatever the output dir is) instead of checking the user's original, local copy of the repo. This also means that remote URLs can be used in your eleventy config. An explanation for the before hook change is in wiki/projects/clone-early.md. Basically, it seems like a plugin can't actually have a before hook run before the site generation happens. So instead, I'm exporting the before hook function so that the user can manually add it as a before hook in the eleventy config for their main site.
25 26 27 28 29
const ajv = new Ajv()
const exec = util.promisify(childProcess.exec)
export default async function repoViewer(eleventyConfig: any, reposConfiguration: ReposConfiguration) {
const validator = ajv.compile(ConfigSchema)
const valid = validator(reposConfiguration)25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
const ajv = new Ajv()
const exec = util.promisify(childProcess.exec)
// TODO document how people need to do this and why. And maybe file 11ty bug?
export function beforeHook(eleventyConfig, reposConfiguration) {
return async ({directories}) => {
const cwd = process.cwd()
const reposPath = reposConfiguration.path || ""
// Check to see if there is already a repo in all of the locations
// that should have one.
for (let repoName in reposConfiguration.repos) {
const repoConfig = reposConfiguration.repos[repoName]
const repoPath = directories.output + reposPath + "/" + eleventyConfig.getFilter("slugify")(repoName)
const gitRepoName = eleventyConfig.getFilter("slugify")(repoName) + ".git"
// If it is there, do git pull
if (fsImport.existsSync(repoPath + ".git")) {
// git repos are just in the repos folder, not in their subdir
// create string of commands saying 'git fetch origin branch:branch' for each branch
const location = directories.output + reposPath + "/" + gitRepoName
const fetchCommands = repoConfig.branchesToPull.map(branch => `git -C ${location} fetch origin ${branch}:${branch}`).join('; ')
await exec(`${fetchCommands} && git -C ${location} update-server-info`)
} else {
// If it is not there, do git clone
// todo: does this work if the latest branch is not checked
// out locally?
let originalLocation = cwd + "/" + repoConfig.location
if (repoConfig.location.startsWith("https://") || repoConfig.location.startsWith("ssh://")) {
originalLocation = repoConfig.location
}
await exec(`git clone ${originalLocation} ${directories.output + reposPath + "/" + gitRepoName} --bare`)
await exec(`git -C ${directories.output + reposPath + "/" + gitRepoName} update-server-info`)
}
const location = directories.output + reposPath + "/" + gitRepoName
await exec(`git -C ${location} symbolic-ref HEAD refs/heads/${repoConfig.defaultBranch}`)
}
}
}
export default async function repoViewer(eleventyConfig: any, reposConfiguration: ReposConfiguration) {
const validator = ajv.compile(ConfigSchema)
const valid = validator(reposConfiguration)34 35 36 37 38 39
eleventyConfig.addTemplateFormats("js")
const reposData = await repos(reposConfiguration)
// TODO: make a better way of making this default to "" so that it doesn't have to
// be done again in src/repos.ts.
const reposPath = reposConfiguration.path || ""34 35 36 37 38 39
eleventyConfig.addTemplateFormats("js")
const reposData = await repos(reposConfiguration, eleventyConfig.dir.output)
// TODO: make a better way of making this default to "" so that it doesn't have to
// be done again in src/repos.ts.
const reposPath = reposConfiguration.path || ""61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
[css]: `${reposPath}/css`,
})
eleventyConfig.on(
"eleventy.before",
async ({ directories }) => {
const cwd = process.cwd()
// Check to see if there is already a repo in all of the locations
// that should have one.
for (let repoName in reposConfiguration.repos) {
const repoConfig = reposConfiguration.repos[repoName]
const repoPath = eleventyConfig.dir.output + reposPath + "/" + eleventyConfig.getFilter("slugify")(repoName)
const gitRepoName = eleventyConfig.getFilter("slugify")(repoName) + ".git"
// If it is there, do git pull
if (fsImport.existsSync(repoPath + ".git")) {
// git repos are just in the repos folder, not in their subdir
// create string of commands saying 'git fetch origin branch:branch' for each branch
const location = eleventyConfig.dir.output + reposPath + "/" + gitRepoName
const fetchCommands = repoConfig.branchesToPull.map(branch => `git -C ${location} fetch origin ${branch}:${branch}`).join('; ')
await exec(`${fetchCommands} && git -C ${location} update-server-info`)
} else {
// If it is not there, do git clone
// todo: does this work if the latest branch is not checked
// out locally?
const originalLocation = cwd + "/" + repoConfig.location
await exec(`git clone ${originalLocation} ${eleventyConfig.dir.output + reposPath + "/" + gitRepoName} --bare`)
await exec(`git -C ${eleventyConfig.dir.output + reposPath + "/" + gitRepoName} update-server-info`)
}
const location = eleventyConfig.dir.output + reposPath + "/" + gitRepoName
await exec(`git -C ${location} symbolic-ref HEAD refs/heads/${repoConfig.defaultBranch}`)
}
}
)
eleventyConfig.on(
"eleventy.after",
async ({ directories }) => {61 62 63 64 65
[css]: `${reposPath}/css`,
})
eleventyConfig.on(
"eleventy.after",
async ({ directories }) => {221 222 223 224 225 226
return ""
}
const location = getLocation(reposConfiguration, repo)
return operations.getFileLastTouchInfo(branch, filename, location)
})
221 222 223 224 225 226
return ""
}
const location = getLocation(reposConfiguration, eleventyConfig.dir.output, repo)
return operations.getFileLastTouchInfo(branch, filename, location)
})
235 236 237 238 239 240
})
eleventyConfig.addAsyncFilter("getFileContents", async (repo: string, branch: string, filename: string) => {
const location = getLocation(reposConfiguration, repo)
const command = `git -C ${location} show ${branch}:${filename}`
const res = await exec(command)
return res.stdout235 236 237 238 239 240
})
eleventyConfig.addAsyncFilter("getFileContents", async (repo: string, branch: string, filename: string) => {
const location = getLocation(reposConfiguration, eleventyConfig.dir.output, repo)
const command = `git -C ${location} show ${branch}:${filename}`
const res = await exec(command)
return res.stdout254 255 256 257 258 259
})
eleventyConfig.addAsyncFilter("getReadMe", async (repoName: string, branchName: string) => {
const location = getLocation(reposConfiguration, repoName)
const command = `git -C ${location} show ${branchName}:README.md`
try {
const res = await exec(command)254 255 256 257 258 259
})
eleventyConfig.addAsyncFilter("getReadMe", async (repoName: string, branchName: string) => {
const location = getLocation(reposConfiguration, eleventyConfig.dir.output, repoName)
const command = `git -C ${location} show ${branchName}:README.md`
try {
const res = await exec(command)69 70 71 72 73 74 75 76
return hunks
}
const getLocation = (reposConfig: ReposConfiguration, repoName: string): string => {
const config = reposConfig.repos[repoName]
return config.location
}
export {69 70 71 72 73 74 75
return hunks
}
const getLocation = (reposConfig: ReposConfiguration, outputDir: string, repoName: string): string => {
return outputDir + (reposConfig.path || "") + "/" + repoName + ".git"
}
export {25 26 27 28 29 30
let cachedRepos: Array<Repository> | null = null
const repos: (reposConfig: ReposConfiguration) => Promise<Array<Repository>> = async (reposConfig) => {
if (cachedRepos !== null) { return cachedRepos }
const repoNames = Object.keys(reposConfig.repos)25 26 27 28 29 30
let cachedRepos: Array<Repository> | null = null
const repos: (reposConfig: ReposConfiguration, outputDir: string) => Promise<Array<Repository>> = async (reposConfig, outputDir) => {
if (cachedRepos !== null) { return cachedRepos }
const repoNames = Object.keys(reposConfig.repos)35 36 37 38 39 40 41 42 43 44 45
const branchNames = getBranchNames(reposConfig.repos[repoName])
const commits: Repository['commits'] = new Map()
for (const branchName of branchNames) {
const repoLocation = getLocation(reposConfig, repoName)
await addBranchToCommitsMap(branchName, repoLocation, commits)
}
const branches = await Promise.all(branchNames.map(async (branchName) => {
const repoLocation = getLocation(reposConfig, repoName)
const branchHeadRes = await exec(`git -C ${repoLocation} show-ref refs/heads/${branchName}`)
const branchHead = branchHeadRes.stdout.split(" ")[0]
return {35 36 37 38 39 40 41 42 43 44 45
const branchNames = getBranchNames(reposConfig.repos[repoName])
const commits: Repository['commits'] = new Map()
for (const branchName of branchNames) {
const repoLocation = getLocation(reposConfig, outputDir, repoName)
await addBranchToCommitsMap(branchName, repoLocation, commits)
}
const branches = await Promise.all(branchNames.map(async (branchName) => {
const repoLocation = getLocation(reposConfig, outputDir, repoName)
const branchHeadRes = await exec(`git -C ${repoLocation} show-ref refs/heads/${branchName}`)
const branchHead = branchHeadRes.stdout.split(" ")[0]
return {12 13
- Where to do this?
- Currently, repo is clone in `main.ts` in an `eleventyConfig.on("eleventy.after")` callback
- There is also an `eleventyConfig.on("eleventy.before")` that you can use12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
- Where to do this?
- Currently, repo is clone in `main.ts` in an `eleventyConfig.on("eleventy.after")` callback
- There is also an `eleventyConfig.on("eleventy.before")` that you can use
- this was surprisingly easy; just changing eleventy.before to eleventy.after
seems to have worked for doing the clone before the site generation.
- the build steps happen in their own, separate eleventy.after callback
now. Which also works, and might have fixed a race condition where it was
sometimes running the build steps before the cloned repo was available.
- Next step: do not reference the original repository after the clone happens.
Always reference the cloned repo instead.
Places that mention `git -C`. Check if these need to be replaced:
- [x] main.ts: const fetchCommands = repoConfig.branchesToPull.map(branch => `git -C ${location} fetch origin ${branch}:${branch}`).join('; ')
- [x] main.ts: await exec(`${fetchCommands} && git -C ${location} update-server-info`)
- [x] main.ts: await exec(`git -C ${eleventyConfig.dir.output + reposPath + "/" + gitRepoName} update-server-info`)
- [x] main.ts: await exec(`git -C ${location} symbolic-ref HEAD refs/heads/${repoConfig.defaultBranch}`)
- [x] main.ts: await exec(`git -C ${tempDirRepoPath} checkout ${branch}`)
- [x] main.ts: const command = `git -C ${location} show ${branch}:${filename}`
- [x] main.ts: const command = `git -C ${location} show ${branchName}:README.md`
- [x] src/repos.ts: const branchHeadRes = await exec(`git -C ${repoLocation} show-ref refs/heads/${branchName}`)
- [x] src/vcses/git/operations.ts: const command = `git -C ${repoLocation} ls-tree -r --name-only ${branchName}`
- [x] src/vcses/git/operations.ts: const totalPatchesCountRes = await exec(`git -C ${repoLocation} rev-list --count ${branchName}`)
- [x] src/vcses/git/operations.ts: const gitLogSubsetRes = await exec(`git -C ${repoLocation} log ${branchName} -p -n 10 --skip ${i}`)
- [x] src/vcses/git/operations.ts: const command = `git -C ${repoLocation} blame --porcelain ${branch} ${filename}`
^ Most of those were fixed by changing what repo location is found in
`getLocation`.
### Before hook issue
Seems like a plugin actually can't make its before hook get triggered before the site generation happens.
Because the plugin is now exclusively reading things from the _cloned_ repository, and not the source repo
on the computer, the before hook really does need to happen before the site generation happens. Apparently
it wasn't happening before, but this wasn't an issue because the plugin was just reading directly from the
source repo.
I've worked around this by not calling the `eleventyConfig.on("eleventy.before")` _in_ the plugin, but rather,
exporting a function that the user will need to call as a before hook in their own eleventy config. Also, it needs
to be `"beforeConfig"` instead of just `"before"` -- seems like plugins are executed as part of the config step.