Change the before hook to an exported function

2ecee51488bef605eff051f8a03644ef2b74e81d

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.
main.ts:26
Before
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)
After
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)
main.ts:35
Before
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 || ""
After
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 || ""
main.ts:62
Before
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 }) => {
After
61
62
63
64
































65
    [css]: `${reposPath}/css`,
  })

  eleventyConfig.on(
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
    "eleventy.after",
    async ({ directories }) => {
main.ts:222
Before
221
222
223
224
225
226
      return ""
    }

    const location = getLocation(reposConfiguration, repo)
    return operations.getFileLastTouchInfo(branch, filename, location)
  })
After
221
222
223
224
225
226
      return ""
    }

    const location = getLocation(reposConfiguration, eleventyConfig.dir.output, repo)
    return operations.getFileLastTouchInfo(branch, filename, location)
  })
main.ts:236
Before
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.stdout
After
235
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.stdout
main.ts:255
Before
254
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)
After
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)
src/helpers.ts:70
Before
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 {
After
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 {
src/repos.ts:26
Before
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)
After
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)
src/repos.ts:36
Before
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 {
After
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 {
wiki/projects/clone-early.md:13
Before
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 use
After
12
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.