Add configuration defaults that are merged with user's config

90eb8e5400897aaf759560f3e3c1132010ff0d9f

Tucker McKnight <tmcknight@instructure.com> | Sat Jul 25 2026

Add configuration defaults that are merged with user's config

Also adds a new type, ReposConfigurationWithDefaultsApplied, which
describes the configuration object with the defaults applied, meaning
that fields that used to be optional are now guaranteed to be
present. So it's the ReposConfiguration object with fewer things
marked optional.
getConfig.ts:0
Before
After
js_templates/common/htmlPage.ts:1
Before
0
1
2
3
4
5





6
7
8
import m from 'mithril'
import { type ReposConfiguration } from '../../src/configTypes.ts'
import { type Repository } from '../../src/dataTypes.ts'
import { NavHelper } from '../helpers/nav.ts'
import relDropDown from './relDropDown.ts'

⁣
⁣
⁣
⁣
⁣
export default async (reposConfig: ReposConfiguration, eleventyConfig: any, data: any, pageContent: any) => {
  // this still necessary?
  if (data.currentRepo === '') {
    return
After
0
1
2
3
4
5
6
7
8
9
10
11
12
13
import m from 'mithril'
import { type ReposConfigurationWithDefaultsApplied } from '../../src/configTypes.ts'
import { type Repository } from '../../src/dataTypes.ts'
import { NavHelper } from '../helpers/nav.ts'
import relDropDown from './relDropDown.ts'

export default async (
  reposConfig: ReposConfigurationWithDefaultsApplied,
  eleventyConfig: any,
  data: any,
  pageContent: any
) => {
  // this still necessary?
  if (data.currentRepo === '') {
    return
js_templates/helpers/nav.ts:1
Before
0
1
2
import { type ReposConfiguration } from "../../src/configTypes.ts"

/**
 * Returns links to various pages used by the default template. Will return a link
After
0
1
2
import { type ReposConfigurationWithDefaultsApplied } from "../../src/configTypes.ts"

/**
 * Returns links to various pages used by the default template. Will return a link
js_templates/helpers/nav.ts:9
Before
8
9
10
11
12
13
14
15
16
17
18
19
20
 * it'll give you /branch/main/commits.
 */
type CurrentRefArgs = {
  reposConfig: ReposConfiguration,
  slugify: Function,
  currentRepoName: string,
  currentRefName: string,
  currentRefType: 'branch' | 'tag' | 'commit'
}
export const NavHelper = (args: CurrentRefArgs) => {
  const reposPath = args.reposConfig.path || ""

  // These two aren't actually used by any pages, but they're in almost
  // every page URL. E.g. all of them start with 'repos/my-repo-name'
After
8
9
10
11
12
13
14
15
16
17
18
19
20
 * it'll give you /branch/main/commits.
 */
type CurrentRefArgs = {
  reposConfig: ReposConfigurationWithDefaultsApplied,
  slugify: Function,
  currentRepoName: string,
  currentRefName: string,
  currentRefType: 'branch' | 'tag' | 'commit'
}
export const NavHelper = (args: CurrentRefArgs) => {
  const reposPath = args.reposConfig.path

  // These two aren't actually used by any pages, but they're in almost
  // every page URL. E.g. all of them start with 'repos/my-repo-name'
js_templates/helpers/nav.ts:109
Before
108
109
110
111
112
113

      return `${refPath(refName, refType)}/commits.xml`
    },
    homepageButtons: args.reposConfig.repos[args.currentRepoName].defaultTemplate?.homepageButtons || []
  }
}
After
108
109
110
111
112
113

      return `${refPath(refName, refType)}/commits.xml`
    },
    homepageButtons: args.reposConfig.repos[args.currentRepoName].defaultTemplate.homepageButtons
  }
}
js_templates/index.ts:7
Before
6
7
8
9
10
11
  const pageContent = m('div', {class: "container"}, [
    m('div', {class: "row my-3"},
      m('div', {class: "col"},
        m('h1', data.reposConfig.defaultTemplate?.allRepositoriesPageTitle || "All Repositories")
      )
    ),
    m('div', {class: "row d-flex flex-wrap"},
After
6
7
8
9
10
11
  const pageContent = m('div', {class: "container"}, [
    m('div', {class: "row my-3"},
      m('div', {class: "col"},
        m('h1', data.reposConfig.defaultTemplate.allRepositoriesPageTitle)
      )
    ),
    m('div', {class: "row d-flex flex-wrap"},
js_templates/index.ts:30
Before
29
30
31
32
33
34
                  class: "mx-1 my-2 btn btn-outline-secondary shadow-none dropdown-toggle clone-popover-btn",
                  'data-copy-text': repo.cloneUrl
                }, 'Clone'),
                (data.reposConfig.repos[repo.name].defaultTemplate?.homepageButtons || []).map((button) => {
                  return m('a', {
                    class: "mx-1 my-2 btn btn-outline-secondary shadow-none",
                    href: button.url,
After
29
30
31
32
33
34
                  class: "mx-1 my-2 btn btn-outline-secondary shadow-none dropdown-toggle clone-popover-btn",
                  'data-copy-text': repo.cloneUrl
                }, 'Clone'),
                (data.reposConfig.repos[repo.name].defaultTemplate.homepageButtons).map((button) => {
                  return m('a', {
                    class: "mx-1 my-2 btn btn-outline-secondary shadow-none",
                    href: button.url,
js_templates/repo.ts:64
Before
63
64
65

66
67
68
  const largestPercent = Math.max(...topLanguagePercentages.map(tuple => tuple[1]), otherLanguagePercent)

  const readmeContent = await renderContentIfAvailable(await getReadMe(repo.name, ref.name), ref.name)
⁣
  const colors = reposConfig.defaultTemplate?.colors?.languageGraph || []

  const pageContent = [
    m('div', {class: "row"}, [
After
63
64
65
66
67
68
69
  const largestPercent = Math.max(...topLanguagePercentages.map(tuple => tuple[1]), otherLanguagePercent)

  const readmeContent = await renderContentIfAvailable(await getReadMe(repo.name, ref.name), ref.name)
  // TODO: make dark mode work here
  const colors = reposConfig.defaultTemplate.colors.languageGraph

  const pageContent = [
    m('div', {class: "row"}, [
js_templates/repo.ts:137
Before
136
137
138
139
140
141
                  ]),
                  m('div', {class: "flex-grow-1 d-flex flex-column"}, [
                    topLanguagePercentages.map((percentTuple, index) => {
                      const color = colors[index % (colors.length - 1)]
                      return m('div', {
                        class: 'language-col flex-grow-1',
                        style: `width: ${percentTuple[1] / largestPercent * 100}%; background-color: ${color};`
After
136
137
138
139
140
141
                  ]),
                  m('div', {class: "flex-grow-1 d-flex flex-column"}, [
                    topLanguagePercentages.map((percentTuple, index) => {
                      const color = colors[index % (colors.length - 1)].light
                      return m('div', {
                        class: 'language-col flex-grow-1',
                        style: `width: ${percentTuple[1] / largestPercent * 100}%; background-color: ${color};`
js_templates/repo.ts:146
Before
145
146
147
148
149
150
                    otherLanguagePercent > 0
                      ? m('div', {
                        class: 'language-col flex-grow-1',
                        style: `width: ${otherLanguagePercent / largestPercent * 100}%; background-color: ${colors[colors.length - 1]};`
                      })
                      : null
                  ])
After
145
146
147
148
149
150
                    otherLanguagePercent > 0
                      ? m('div', {
                        class: 'language-col flex-grow-1',
                        style: `width: ${otherLanguagePercent / largestPercent * 100}%; background-color: ${colors[colors.length - 1].light};`
                      })
                      : null
                  ])
main.ts:7
Before
6
7
8



9
10
11
12
13
14
import { flatPatches, commitsInRepo } from './src/flatPatches.ts'
import paginatedPatches, {type PatchPage} from './src/paginatedPatches.ts'
import {getLocation} from './src/helpers.ts'
⁣
⁣
⁣
import {ReposConfiguration} from './src/configTypes.ts'
import { type SortedFileList } from './src/dataTypes.ts'
import {Ajv} from 'ajv'
import ConfigSchema from './schemas/ReposConfiguration.json' with { type: 'json' }
import commonPage from './js_templates/common/commonPage.ts'
import repoJsTemplate from './js_templates/repo.ts'
import filesJsTemplate from './js_templates/files.ts'
After
6
7
8
9
10
11
12
13


14
15
import { flatPatches, commitsInRepo } from './src/flatPatches.ts'
import paginatedPatches, {type PatchPage} from './src/paginatedPatches.ts'
import {getLocation} from './src/helpers.ts'
import {
  ReposConfiguration,
  ReposConfigurationWithDefaultsApplied
} from './src/configTypes.ts'
import { type SortedFileList } from './src/dataTypes.ts'
⁣
⁣
import commonPage from './js_templates/common/commonPage.ts'
import repoJsTemplate from './js_templates/repo.ts'
import filesJsTemplate from './js_templates/files.ts'
main.ts:23
Before
22
23
24
25
26

27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import rawJsTemplate from './js_templates/raw.ts'
import feedJsTemplate from './js_templates/feed.ts'
import { NavHelper } from './js_templates/helpers/nav.ts'

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: any, reposConfiguration: ReposConfiguration) {
  const validator = ajv.compile(ConfigSchema)
  const valid = validator(reposConfiguration)
  if (!valid) {
    throw new Error(validator.errors.map(error => `config object at ${error.instancePath.replaceAll("/", ".")}: ${error.message}\n${Object.values(error.params).toString()}`).join("\n"))
  }

  return async ({directories}) => {
    const cwd = process.cwd()
    const reposPath = reposConfiguration.path || ""
    const slugify = eleventyConfig.getFilter("slugify")
    // Check to see if there is already a repo in all of the locations
    // that should have one.
After
22
23
24

25
26
27
28
29
30



31
32

33
34
35
36
37
import rawJsTemplate from './js_templates/raw.ts'
import feedJsTemplate from './js_templates/feed.ts'
import { NavHelper } from './js_templates/helpers/nav.ts'
⁣
import { getConfig } from './getConfig.ts'

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: any, userConfiguration: ReposConfiguration) {
⁣
⁣
⁣
  const reposConfiguration: ReposConfigurationWithDefaultsApplied = getConfig(userConfiguration)

⁣
  return async ({directories}) => {
    const cwd = process.cwd()
    const reposPath = reposConfiguration.path
    const slugify = eleventyConfig.getFilter("slugify")
    // Check to see if there is already a repo in all of the locations
    // that should have one.
main.ts:81
Before
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
  }
}

export default async function repoViewer(eleventyConfig: any, reposConfiguration: ReposConfiguration) {
  const validator = ajv.compile(ConfigSchema)
  const valid = validator(reposConfiguration)
  if (!valid) {
    throw new Error(validator.errors.map(error => `config object at ${error.instancePath.replaceAll("/", ".")}: ${error.message}`).join("\n"))
  }

  const slugify = eleventyConfig.getFilter("slugify")
  const reposData = await repos(reposConfiguration, eleventyConfig.dir.output, slugify)
  // 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 || ""

  eleventyConfig.addGlobalData("repos", reposData)
  eleventyConfig.addGlobalData("reposConfig", reposConfiguration)
After
80
81
82
83



84
85

86
87
88
89
90
91
92
  }
}

export default async function repoViewer(eleventyConfig: any, userConfiguration: ReposConfiguration) {
⁣
⁣
⁣
  const reposConfiguration: ReposConfigurationWithDefaultsApplied = getConfig(userConfiguration)

⁣
  const slugify = eleventyConfig.getFilter("slugify")
  const reposData = await repos(reposConfiguration, eleventyConfig.dir.output, slugify)
  // 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

  eleventyConfig.addGlobalData("repos", reposData)
  eleventyConfig.addGlobalData("reposConfig", reposConfiguration)
main.ts:248
Before
247
248
249
250
251
252
    let filenameParts = filename.split(".")
    let extension = filenameParts[filenameParts.length - 1]
    const extensionsConfig = reposConfiguration.repos[repoName].languageExtensions
    return extensionsConfig && extensionsConfig[extension] ? extensionsConfig[extension] : extension
  })

  eleventyConfig.addFilter("topLevelFilesOnly", (files: Array<string>, currentLevel: string): SortedFileList => {
After
247
248
249
250
251
252
    let filenameParts = filename.split(".")
    let extension = filenameParts[filenameParts.length - 1]
    const extensionsConfig = reposConfiguration.repos[repoName].languageExtensions
    return extensionsConfig[extension] ? extensionsConfig[extension] : extension
  })

  eleventyConfig.addFilter("topLevelFilesOnly", (files: Array<string>, currentLevel: string): SortedFileList => {
schemas/ReposConfiguration.json:163
Before
162
163
164



165









166
167
              "properties": {
                "languageGraph": {
                  "items": {
⁣
⁣
⁣
                    "type": "string"
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
                  },
                  "type": "array"
                }
After
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
              "properties": {
                "languageGraph": {
                  "items": {
                    "additionalProperties": false,
                    "properties": {
                      "dark": {
                        "type": "string"
                      },
                      "light": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "light"
                    ],
                    "type": "object"
                  },
                  "type": "array"
                }
src/configTypes.ts:1
Before
0


























1
/**
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
 The ReposConfiguration object contains information about your local repositories,
 like their name and location on your local filesystem. Add repositories to this
After
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/** @ignore **/
export type ReposConfigurationWithDefaultsApplied = ReposConfiguration & {
  path: string,
  repos: {
    [repoName: string]: GitConfigWithDefaultsApplied
  },
  defaultTemplate: {
    enabled: boolean,
    allRepositoriesPageTitle: string,
    colors: {
      languageGraph: Array<any>,
    }
  }
}

/** @ignore **/
export type GitConfigWithDefaultsApplied = GitConfig & {
  defaultBranch: string,
  branches: Array<any>,
  tags: Array<any>,
  languageExtensions: Object,
  defaultTemplate: {
    homepageButtons: Array<any>
  }
}

/**
 The ReposConfiguration object contains information about your local repositories,
 like their name and location on your local filesystem. Add repositories to this
src/configTypes.ts:57
Before
56
57
58
59
60
61
    enabled?: boolean,
    allRepositoriesPageTitle?: string,
    colors?: {
      languageGraph?: Array<string>,
    },
  },
}
After
56
57
58
59
60
61
    enabled?: boolean,
    allRepositoriesPageTitle?: string,
    colors?: {
      languageGraph?: Array<{light: string, dark?: string}>,
    },
  },
}
src/helpers.ts:2
Before
1
2
3
4
5
6
import escape from 'escape-html'
import * as Diff from 'diff'
import {type Repository} from './dataTypes.ts'
import { type ReposConfiguration } from './configTypes.ts'

type Diffs = ReturnType<Repository['commits']['get']>['diffs']
After
1
2
3
4
5
6
import escape from 'escape-html'
import * as Diff from 'diff'
import {type Repository} from './dataTypes.ts'
import { type ReposConfigurationWithDefaultsApplied } from './configTypes.ts'

type Diffs = ReturnType<Repository['commits']['get']>['diffs']
src/helpers.ts:98
Before
97
98
99
100
101
102
103
  return hunks
}

const getLocation = (reposConfig: ReposConfiguration, outputDir: string, repoName: string, slugify: Function): string => {
  return outputDir + (reposConfig.path || "") + "/" + slugify(repoName) + ".git"
}

const readChunkedCommandOutput = async (executable, args): Promise<string> => {
After
97
98
99
100
101
102
103
  return hunks
}

const getLocation = (reposConfig: ReposConfigurationWithDefaultsApplied, outputDir: string, repoName: string, slugify: Function): string => {
  return outputDir + (reposConfig.path) + "/" + slugify(repoName) + ".git"
}

const readChunkedCommandOutput = async (executable, args): Promise<string> => {
src/repos.ts:1
Before
0
1
2
3
import {
  type ReposConfiguration,
  type GitConfig,
} from './configTypes.ts'
import util from 'util'
After
0
1
2
3
import {
  type ReposConfigurationWithDefaultsApplied,
  type GitConfig,
} from './configTypes.ts'
import util from 'util'
src/repos.ts:18
Before
17
18
19
20
21
22
const tagsForReposMap: Map<string, Array<{name: string}>> = new Map()

const getBranchesAndTags = async (
  reposConfig: ReposConfiguration,
  repoName: string,
  outputDir: string,
  slugify: Function,
After
17
18
19
20
21
22
const tagsForReposMap: Map<string, Array<{name: string}>> = new Map()

const getBranchesAndTags = async (
  reposConfig: ReposConfigurationWithDefaultsApplied,
  repoName: string,
  outputDir: string,
  slugify: Function,
src/repos.ts:148
Before
147
148
149




150
151
152
// The key for this map is a string like "filename-sha".
const fileMap: Map<string, FileInfo> = new Map()

⁣
⁣
⁣
⁣
const repos: (reposConfig: ReposConfiguration, outputDir: string, slugify: Function) => Promise<Array<Repository>> = async (reposConfig, outputDir, slugify) => {
  if (cachedRepos !== null) { return cachedRepos }

  const repoNames = Object.keys(reposConfig.repos)
After
147
148
149
150
151
152
153
154
155
156
// The key for this map is a string like "filename-sha".
const fileMap: Map<string, FileInfo> = new Map()

const repos: (
  reposConfig: ReposConfigurationWithDefaultsApplied,
  outputDir: string,
  slugify: Function
) => Promise<Array<Repository>> = async (reposConfig, outputDir, slugify) => {
  if (cachedRepos !== null) { return cachedRepos }

  const repoNames = Object.keys(reposConfig.repos)