Where JavaScript Goes Without a Module System
A 40-line entry script that is entirely commented out, thirty-one views carrying inline script blocks, a 2,904-line page file, and @section as the only architecture - the addendum's closing ledger.
The file the project template created to be the application's JavaScript home is forty lines long, and every one of the forty is commented out. It sits where the template put it, at the top of the scripts folder, and the fossilised lines still describe a real feature — an early behaviour someone wrote there, then disabled, line by line, until the file was all comment and no code. The application kept working, because by then nothing loaded it. The entry point was dead; the JavaScript had simply gone to live everywhere else.
Where “everywhere else” is, precisely, is this final chapter — the last of the frontend gaps that CSS by Accretion and its predecessors have been walking, and the one that explains the other three. The string-built modals, the hand-stitched validation, the bundles nobody could account for: all of them are symptoms of a codebase with no unit of JavaScript organisation. Not a bad module system — none. What follows is where the code settles when there is no shape for it to take.
Where the code went instead
The census, from a survey of the shipped application:
- Thirty-one views carry inline
<script>blocks — logic written directly into Razor markup, per page, per feature, per Friday. - The layout is a program. Of
_Layout.cshtml's 489 lines, roughly 260 are a single inline script block: the office-visit modal flow from the previous two chapters, the profile-image upload, the sidebar toggle, and an employee-locator table, all global, all running on every page of the application. - The big screens got big files. The report dashboard's page script runs to 2,904 lines; a client-configuration screen owns 1,080; an admin screen 720. Alongside them sits a vendored date-time picker of 2,637 lines — a fork-by-copy of a plugin, maintained (which is to say, not) in-tree.
- Thirty-three
@sectiondeclarations across eighteen views wire page scripts and styles to the layout — the closest thing the codebase has to an import statement.
The 2,904-line file rewards a moment of attention, because a file like that is not 2,904 lines of one thing. It is event handlers, AJAX calls, HTML-string rendering, date formatting, state flags, and drag-drop wiring, interleaved in the order history added them, sharing one scope. Functions in such a file cannot be tested — there is no export to import, so the only way to invoke anything is to stand the page up and click. They cannot be safely renamed — callers may lurk in any of thirty-one inline blocks or a Razor onclick attribute. And they cannot be deleted with confidence, for the same reason — which is how a forty-line file becomes 100 per cent comments instead of being removed: in a codebase without modules, commenting-out is the only delete that feels safe, because nobody can enumerate the callers of anything.
@section is the architecture
To give the estate its due, the per-page wiring was a real convention, consistently applied. A report screen, in the house style (fresh illustrative code, as throughout):
@section Styles {
<link rel="stylesheet" href="~/bundles/report-builder.min.css" />
}
<div id="reportRoot" data-emp="@Model.EmployeeRef" data-template="@Model.DefaultTemplate">
<!-- server-rendered shell -->
</div>
@section Scripts {
<script src="~/bundles/attendance-reports.min.js"></script>
<script>
$(function () {
initReportDashboard($('#reportRoot').data('emp'),
$('#reportRoot').data('template'));
});
</script>
}
This buys two genuine things: ordering (page scripts render after the layout's bundles, so jQuery exists when the page code runs) and opt-in (only report pages pay for the report bundle — the discipline part one showed the table bundle never received). It is not nothing. It is also the entire dependency story. initReportDashboard is a global; whether it exists depends on which bundle the section loaded; whether the bundle's files concatenated in a working order is checked by no tool and declared nowhere. Load order is the dependency graph, and the graph is maintained by superstition.
Client state got the same non-architecture. There is no store; there are querystrings (?eid=…&year=… threaded through every navigation), ViewBag values interpolated into script blocks, claims surfaced via the layout, data-* attributes on container divs — the data-emp above is the idiom — and a handful of window globals for whatever remained. The AJAX partial-HTML swaps those globals orchestrate are a story the parent series already owns — Resurrecting Html.Action in ASP.NET Core — so I will only note here that composition-by-server-HTML is precisely why so little client state existed at all: the server held the state, and the client held fragments of rendered truth with no idea what they meant. Clever, dated, and surprisingly robust — until any two fragments needed to agree with each other without a round trip.
The widget zoo
The last habitat is the widgets. The platform's UI personality came from vendored jQuery plugins, and the collection was a zoo: a modal library, a toast library, DataTables, guided tours (with the six copied themes of part three), an org-chart renderer, slide-out panels, animated number counters — and, on the login page, a particle-field background drifting behind the sign-in button. The dashboards' circular gauge cards get exactly one sentence here, because their real story — the same KPI arriving at the same arc from three different formulas — is told in One Metric, Three Answers.
Each plugin, taken alone, was a rational purchase: real need, working library, one script tag. The zoo is what the purchases sum to when nothing curates them. Every widget brings its own initialisation idiom, its own event vocabulary, its own styling opinions and its own lifecycle bugs — ten small frameworks in one page, integrated only by the shared $. The particle background is my favourite exhibit, not because it was expensive — it was a decorative flourish on one page — but because of what it reveals about acquisition cost: when adding a library is one script tag, the marginal cost of a widget rounds to zero at commit time and the total cost surfaces years later, six megabytes at a time. Nobody decides to own a zoo. They decide, one Friday at a time, to adopt one more animal.
Closing the frontend ledger
Four chapters, then, and one pattern wearing four costumes. The unminified bundles are the dependency graveyard pattern shipped to the browser. The validation theatre is referenced-but-never-called, client-side. Site2.css is the copy-instead-of-change habit rendered in stylesheets, and the module-less JavaScript is the same absence of seams that left the server with four copied dashboard controllers. The parent series ended by observing that its codebase's defects were mostly one defect — no safety net, so no safe way to share instead of copy — and the frontend obeys the same law. A frontend without modules, conventions, or a build step is not a different kind of codebase from the backend behind it; it is the same team's risk calculus, running where the linters can't see.
What I would do differently, in the order I would actually do it:
- Turn the build tools on — minification first (part one's eight lines of JSON), then any linter at all; an unused-file report would have buried the dead entry script years earlier.
- Give every screen a module seam. Even in that era, an IIFE-per-page exposing one
init— or ES modules, which every browser this intranet served already supported — turns 2,904-line files into testable, renameable, deletable units. - Adopt-or-delete for every framework and widget — the validation stack and half the zoo fail the audit immediately; what survives gets an owner and a version.
- Move markup back to the server — string-built modals become partials, which lets the validation framework, the stylesheets and the naming convention all reach them (chapters two and three collapse into this one decision).
- Set a page-weight budget and let CI fail the build that breaks it — because every pattern in these four chapters shipped precisely where no gate stood.
The parent series closed with What a Hand-Rolled Intranet App Taught Me, and its verdict — proud of the platform, clear-eyed about the gap between what the code did and what we believed it did — holds unamended here; the frontend just relocates the gap to the one place that series never looked. These four chapters were the browser's half of that honesty. The estate has now been walked end to end — fifteen parts of platform, eighteen of hinterland, four of frontend — and the most durable thing I own from all of it is still the same: not the system, but the ledger.