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.
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 === '') {
return0 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 === '') {
return0 1 2
import { type ReposConfiguration } from "../../src/configTypes.ts"
/**
* Returns links to various pages used by the default template. Will return a link0 1 2
import { type ReposConfigurationWithDefaultsApplied } from "../../src/configTypes.ts"
/**
* Returns links to various pages used by the default template. Will return a link8 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'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'108 109 110 111 112 113
return `${refPath(refName, refType)}/commits.xml`
},
homepageButtons: args.reposConfig.repos[args.currentRepoName].defaultTemplate?.homepageButtons || []
}
}
108 109 110 111 112 113
return `${refPath(refName, refType)}/commits.xml`
},
homepageButtons: args.reposConfig.repos[args.currentRepoName].defaultTemplate.homepageButtons
}
}
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"},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"},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,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,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"}, [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"}, [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};`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};`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
])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
])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'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'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.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.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)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)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 => {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 => {162 163 164 165 166 167
"properties": {
"languageGraph": {
"items": {
"type": "string"
},
"type": "array"
}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"
}0 1
/**
The ReposConfiguration object contains information about your local repositories,
like their name and location on your local filesystem. Add repositories to this0 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 this56 57 58 59 60 61
enabled?: boolean,
allRepositoriesPageTitle?: string,
colors?: {
languageGraph?: Array<string>,
},
},
}56 57 58 59 60 61
enabled?: boolean,
allRepositoriesPageTitle?: string,
colors?: {
languageGraph?: Array<{light: string, dark?: string}>,
},
},
}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']
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']
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> => {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> => {0 1 2 3
import {
type ReposConfiguration,
type GitConfig,
} from './configTypes.ts'
import util from 'util'0 1 2 3
import {
type ReposConfigurationWithDefaultsApplied,
type GitConfig,
} from './configTypes.ts'
import util from 'util'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,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,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)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)