The most striking finding from this snapshot of 11ty/eleventy at commit d2270d5 is that the risk is entirely in the debt quadrant — zero functions are in fire, meaning nothing is actively regressing right now, but 187 functions are carrying high structural complexity with no recent attention. Across 1,655 total functions, 55 are rated critical. The function I would address first, getMappedDate in src/Template.js, hasn’t been modified in 203 days and has collected 21 bug-linked commits at the file level over its lifetime — a combination that signals high blast radius the moment anyone has to touch it. I would treat all five top hotspots as overdue refactoring work to schedule before the next feature push into these files.
The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).
Top 5 Hotspots
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
_getInstance | src/Engines/JavaScript.js | 15.3 | 28 | 6 | 4 |
exec | cmd.cjs | 15.1 | 27 | 4 | 27 |
getMappedDate | src/Template.js | 13.4 | 18 | 4 | 7 |
I18nPlugin | src/Plugins/I18nPlugin.js | 13.3 | 19 | 3 | 15 |
RenderPlugin | src/Plugins/RenderPlugin.js | 13.0 | 9 | 3 | 36 |
Quadrant Overview
1,655 functions analyzed
The quadrant picture tells a clear story: eleventy is in a maintenance-stable state right now — no fire-quadrant functions means no complex code is being actively churned — but 187 functions have accumulated structural debt that will demand attention eventually. The four watch-quadrant functions (including initServerInstance in src/Serve.js, touched once in the last 30 days, and getFrontMatterParsingOptions in src/TemplateContent.js, last changed 5 days ago) show where active development is actually happening. Both are low-complexity functions, so the current development surface carries modest structural risk. The debt is sitting quietly in the template engine, CLI layer, and plugin subsystems.
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.Long Function×4Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Complex Branching×3Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.God Function×3God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Deeply Nested×1Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.Stale Complex×1Stale Complex
High structural complexity but untouched for a long time — structural debt that will bite whoever opens it next.
Exit-heavy and long-function patterns dominate — all five top hotspots have multiple return paths, and four are flagged as long functions. That combination makes test coverage hard: each exit path is a test case that may or may not exist, and long functions resist unit testing because there is no clean seam at which to inject or assert. The three god-function flags compound this: broad coupling means refactoring any one of these functions carries ripple risk into everything it calls.
_getInstance — src/Engines/JavaScript.js
_getInstance normalises whatever a .11ty.js template file exports — a string, a Buffer, a Promise, a plain function, a class with prototype properties, or a class with instance properties — into a consistent object shape with render and optionally data. That sounds straightforward until you look at the numbers: a cyclomatic complexity of 28 means 28 independent execution paths, and a maximum nesting depth of 6 means some of those paths run six control-structure levels deep. It hasn’t been touched in 82 days.
The source makes the branching concrete. The function opens by checking whether the module is an ESM default export, then fans into a chain of type guards: string-or-Buffer-or-Promise gets one treatment, a function with prototype data or render gets another, a function whose .toString() starts with "class " gets a third path that instantiates the class just to inspect its instance properties (an approach the code itself flags with // JavaScript lol), and a plain object with data or render gets a fourth. Each top-level branch has its own internal conditionals around missing render or data properties — that’s where the nesting depth accumulates. The deeply_nested and exit_heavy pattern flags are both earned here.
The file-level signals add context without proving defect: 13 bug-linked commits and a 50% bug-fix fraction on this file over its history suggest the JavaScript template engine has needed correction before. The current function complexity is a structural explanation for why that might happen again.
Recommendation: The class-detection path — the one that calls mod.toString() to infer whether a function is a class — is the most unusual and fragile branch. I would extract it into a named helper (isClassTemplate or similar) with its own tests. That alone would reduce CC by roughly a third and bring nesting depth down to a more manageable level. The broader target is splitting _getInstance into one function per module-export shape, each testable in isolation.
exec — cmd.cjs
exec is the CLI entry point — the function that fires when you run npx @11ty/eleventy from a terminal. It parses process.argv, sets up error handlers, conditionally enables the Node compile cache, then branches across every supported CLI mode: version, help, serve, watch, build, dry-run, incremental, and more. Its cyclomatic complexity of 27 reflects that decision tree. The fan-out of 27 is the number that concerns me most: this single function calls 27 distinct functions, making it a textbook god function. It hasn’t been changed in 70 days.
The source confirms the picture. Dynamic import() calls are scattered throughout — DebugLogUtil, ErrorHandler, EnvironmentVars, ConsoleLogger, Core, and more are all pulled in lazily at runtime. That’s a reasonable pattern for a CLI startup function trying to stay fast, but it also means static analysis (including fan-out metrics) likely undercounts the true coupling here, since JavaScript dynamic imports can resolve to modules that aren’t visible at parse time. The three process-level event handlers (unhandledRejection, uncaughtException, rejectionHandled) wired up inline add further exit-path complexity.
The external signals are worth noting: two-thirds of the commits to this file (a bug-fix fraction of 0.67) have been bug fixes, across 6 total commits. That’s a small sample, but it suggests this file has required correction more often than it has been extended.
Recommendation: The argument-parsing block and the mode-dispatch block are natural extraction targets. Pulling parseArgv into its own function and dispatchMode into another would cut exec’s CC by more than half and make each piece independently testable. The process-level event handler wiring could move into a dedicated registerProcessHandlers helper. None of these extractions change runtime behaviour — they just create seams for testing.
getMappedDate — src/Template.js
This is the function I’d put at the top of any refactoring queue. getMappedDate resolves a template’s date from whatever source is available: custom date-parsing callbacks registered by the user, a Luxon DateTime instance, a JavaScript Date, a special string keyword ("git last modified", "last modified", "git created", "created"), an ISO string parsed by Luxon, or a date embedded in the file path by regex. It hasn’t been touched in 203 days — the longest dormancy in this top five — and it carries a cyclomatic complexity of 18.
The stale_complex pattern flag is the right label. The function is marked async because two of its branches shell out to git for timestamps, introducing async error surfaces that don’t exist in the other branches. The source shows the branching chain explicitly: it loops over custom date parsers first, then type-checks the result with a sequence of instanceof and constructor.name guards before falling through to string keyword matching. The exit_heavy and long_function patterns both apply, and the fan-out of 7 reflects calls to git utilities, Luxon conversion helpers, and file-stat methods across several branches.
The file-level external signals here are the most significant of any function in this analysis: 21 bug-linked commits and a PR review comment density of 5.0 on src/Template.js. That’s historical signal, not proof of a current defect, but it does suggest this file has attracted disproportionate scrutiny and correction over time. With 203 days of dormancy on a CC-18 function in a file with that history, the blast radius when someone next touches it is real.
Recommendation: Each date-source type should be its own named resolver — resolveLuxonDate, resolveGitDate, resolveKeywordDate, resolveISODate — with getMappedDate reduced to an orchestrator that tries each in sequence. That decomposition would bring CC down to single digits for each piece and make the git-async branches testable without exercising the full resolution chain.
I18nPlugin — src/Plugins/I18nPlugin.js
I18nPlugin is the registration function for eleventy’s built-in internationalisation plugin. When a user adds it to their .eleventy.js config, this function wires up event listeners, global data, filters, and URL-normalisation logic for locale-aware URL generation. Its cyclomatic complexity of 19 and fan-out of 15 reflect that it’s doing a lot of registration work in one place — a classic god-function shape. It hasn’t been touched in 77 days.
The source shows the structure: the function sets defaults, validates that defaultLanguage is provided, attaches listeners for buildawesome.extensionmap and buildawesome.contentmap build events (where the actual locale URL map is computed), registers computed global data for page.lang, and then defines and registers two filters (locale_url and locale_links) whose implementations reference the content maps captured in closure. Each filter has its own branching logic — the locale_url filter alone walks a localeUrlsMap looking for URL matches, then tries prepending the language code, then falls back to checking urlToInputPath — which is where the CC of 19 accumulates despite a nesting depth of only 3.
The exit_heavy, god_function, and long_function pattern flags are all accurate. The file-level signal is notable in a different way: a bug-fix fraction of 1.0 across 2 commits means every commit to this file has been a bug fix. Small sample, but worth keeping in mind.
Recommendation: The filter implementations — particularly the locale_url logic — should be extracted into separately testable functions outside the plugin registration wrapper. Right now, testing the URL normalisation logic requires standing up an entire plugin registration context. Extracting resolveLocaleUrl(url, langCode, contentMaps, options) as a pure function would make it unit-testable and reduce I18nPlugin’s own complexity significantly.
RenderPlugin — src/Plugins/RenderPlugin.js
RenderPlugin is the registration function for eleventy’s render shortcodes (renderTemplate, renderFile, renderContent), which allow templates to render other templates inline. Its cyclomatic complexity of 9 is the lowest of the five hotspots — moderate on its own — but its fan-out of 36 is the highest in this entire analysis. Thirty-six distinct callees means this function touches more of the codebase than any other function in the top five. It hasn’t been changed in 76 days.
The source explains where the fan-out comes from. RenderPlugin registers behaviour for three different template engines — Liquid, Nunjucks, and JavaScript — each requiring its own tag parser and renderer implementation defined inline within the plugin function. Each engine integration calls into that engine’s specific API, into eleventy’s own template rendering pipeline, and into configuration objects, accumulating callees rapidly. The Liquid tag implementation alone defines parse and render methods that call into liquidEngine.parser, Liquid.parseArgumentsBuiltin, liquidEngine.evalValue, and several context accessors. Multiply that across three engines and a filter registration, and 36 fan-out is the predictable result.
In JavaScript, dynamic property access and async callback chains can further obscure dependencies that static analysis doesn’t capture, so 36 may be a floor rather than a ceiling for the true coupling here. The god-function, long-function, and exit-heavy patterns are all present. At fan-out 36, a change to any of those 36 callee interfaces — an engine API update, a template config change, a context shape change — is a potential breakage site in this function.
Recommendation: Each engine’s tag and filter implementation should live in its own module — LiquidRenderTag.js, NunjucksRenderTag.js — and be imported into RenderPlugin rather than defined inline. That decomposition would drop RenderPlugin’s fan-out dramatically and make each engine integration independently testable, leaving the function as a thin orchestrator composing smaller, verifiable pieces.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
exit_heavy | 5 |
long_function | 4 |
complex_branching | 3 |
god_function | 3 |
deeply_nested | 1 |
stale_complex | 1 |
These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.
See more analyses with these patterns: complex_branching, deeply_nested, exit_heavy, god_function, long_function.
Reproduce This Analysis
git clone https://github.com/11ty/eleventy
cd eleventy
git checkout d2270d5dd9af5d5cb5bb0150731eb612bcbd635b
hotspots analyze . --mode snapshot --explain-patterns --force
To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.
I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →