Load the assets as ES modules through an import map - #2737
Merged
Merged
Conversation
Sprockets has no notion of `import`/`export` — its whole JS pipeline is
`concat_javascript_sources`, which appends each file and pokes a `;`
between them. The single global scope that produced is what the `app`
object was a registry for. Serving the modules individually instead
costs ~33 KB of cross-file compression and six levels of import depth,
which `modulepreload` flattens to about one round trip.
Every module is served at a content-digested URL, `immutable` and
cacheable forever, and the import map that points at those URLs ships
inside the HTML, which is never cached. A client therefore reads one
build's map and fetches that build's modules, and can't end up running
half of one build and half of another. Old digests are kept for a couple
of builds so a deploy doesn't strand a client mid-load.
The modules import each other by relative path rather than by a bare
specifier so that tsc, editors and Node resolve them with no extra
configuration; the digests still apply because an import map may key on
a URL as well as on a bare name.
`app` keeps only the state the rest of the app reads off it. The four
class registries are gone, and with them the reason for most of
globals.d.ts. Two things had to move rather than be translated:
- `config` becomes its own leaf module. Five classes read it from a
`static` field, which runs at class-definition time and would find
`app` still uninitialised inside the app/views import cycle. The
same hazard made `static model = Doc` in the collections throw, so
the model is resolved from a method now.
- The injection-error check compared `window.$` against the app's own
copy, to notice extensions replacing the globals. Module scope makes
that impossible, which also makes the comparison always true — it
would have reported every error as an injection error and shown an
alert. Removed along with the fields that existed to feed it.
The vendored libraries assign globals rather than exporting, so they
stay one concatenated classic script loaded ahead of the graph. docs.js
and debug.js stay separate entries: single-doc pages skip the catalog,
and debug has to run before the boot it wraps.
The tests import the modules directly instead of concatenating files
into a `vm` context, with a loader hook standing in for the modules ERB
generates at build time. A new test evaluates the whole graph in the
order the browser does, which is the only thing that catches the
initialisation-cycle bugs above.
There was a problem hiding this comment.
🟡 Changes recommended
The generated news module has an initialization-cycle failure, and the import-map requirement breaks currently advertised browsers.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Migrates DevDocs’ JavaScript assets from a Sprockets-concatenated global bundle to individually cached ES modules resolved through an import map.
Changes:
- Replaces global registries with explicit module imports and exports.
- Adds digested module mapping, preloading, vendor bundling, and asset retention.
- Updates asset tests to load modules directly and verify graph initialization.
File summaries
| File | Description |
|---|---|
views/other.erb |
Loads single-doc pages through modules. |
views/index.erb |
Loads the full module graph and catalog. |
tsconfig.json |
Enables unused-local checking. |
test/assets/tsconfig.json |
Configures Node and DOM test types. |
test/assets/setup.js |
Provides browser globals and generated-module hooks. |
test/assets/search_ranking_test.js |
Converts ranking tests to module imports. |
test/assets/search_hash_test.js |
Converts hash tests to module imports. |
test/assets/module_graph_test.js |
Tests module graph evaluation. |
test/assets/fixtures/root_tmpl.js |
Stubs generated landing templates. |
test/assets/fixtures/news_tmpl.js |
Stubs generated news templates. |
test/assets/fixtures/docs.js |
Stubs the generated catalog module. |
test/assets/fixtures/config.js |
Stubs generated application configuration. |
test/assets/doc_version_test.js |
Converts migration tests to modules. |
package.json |
Enables ESM and test setup hooks. |
lib/tasks/assets.thor |
Retains previous asset generations. |
lib/app.rb |
Builds maps, preloads, and module asset lists. |
assets/javascripts/views/view.js |
Exports the base view and renderer dependency. |
assets/javascripts/views/sidebar/type_list.js |
Modularizes type-list dependencies. |
assets/javascripts/views/sidebar/sidebar.js |
Modularizes the sidebar composition. |
assets/javascripts/views/sidebar/sidebar_hover.js |
Exports the hover view. |
assets/javascripts/views/sidebar/results.js |
Modularizes search results. |
assets/javascripts/views/sidebar/entry_list.js |
Exports the entry list. |
assets/javascripts/views/sidebar/doc_picker.js |
Modularizes the documentation picker. |
assets/javascripts/views/sidebar/doc_list.js |
Modularizes the documentation list. |
assets/javascripts/views/search/search.js |
Modularizes the main search view. |
assets/javascripts/views/search/search_scope.js |
Modularizes scoped search. |
assets/javascripts/views/pages/support_tables.js |
Exports support-table behavior. |
assets/javascripts/views/pages/sqlite.js |
Exports SQLite page behavior. |
assets/javascripts/views/pages/rdoc.js |
Exports RDoc page behavior. |
assets/javascripts/views/pages/jquery.js |
Exports jQuery page behavior. |
assets/javascripts/views/pages/hidden.js |
Modularizes hidden-document pages. |
assets/javascripts/views/pages/base.js |
Exports the base documentation page. |
assets/javascripts/views/misc/updates.js |
Modularizes update notifications. |
assets/javascripts/views/misc/tip.js |
Exports tip notifications. |
assets/javascripts/views/misc/notif.js |
Exports the notification base. |
assets/javascripts/views/misc/notice.js |
Exports persistent notices. |
assets/javascripts/views/misc/news.js |
Modularizes news notifications. |
assets/javascripts/views/list/paginated_list.js |
Modularizes paginated lists. |
assets/javascripts/views/list/list_select.js |
Exports list selection behavior. |
assets/javascripts/views/list/list_fold.js |
Modularizes list folding. |
assets/javascripts/views/list/list_focus.js |
Modularizes list focus behavior. |
assets/javascripts/views/layout/settings.js |
Modularizes the settings panel. |
assets/javascripts/views/layout/resizer.js |
Exports the layout resizer. |
assets/javascripts/views/layout/path.js |
Exports breadcrumb behavior. |
assets/javascripts/views/layout/mobile.js |
Modularizes mobile layout behavior. |
assets/javascripts/views/layout/menu.js |
Exports menu behavior. |
assets/javascripts/views/layout/document.js |
Composes the root view through imports. |
assets/javascripts/views/content/type_page.js |
Modularizes type pages. |
assets/javascripts/views/content/static_page.js |
Exports static-page rendering. |
assets/javascripts/views/content/settings_page.js |
Modularizes settings-page behavior. |
assets/javascripts/views/content/root_page.js |
Exports landing-page behavior. |
assets/javascripts/views/content/offline_page.js |
Modularizes offline management. |
assets/javascripts/views/content/entry_page.js |
Maps document-specific page views. |
assets/javascripts/views/content/content.js |
Composes content views through imports. |
assets/javascripts/vendor.js |
Creates the classic vendor bundle. |
assets/javascripts/tracking.js |
Imports analytics dependencies explicitly. |
assets/javascripts/templates/tip_tmpl.js |
Exports tip templates. |
assets/javascripts/templates/sidebar_tmpl.js |
Exports sidebar templates. |
assets/javascripts/templates/path_tmpl.js |
Exports breadcrumb templates. |
assets/javascripts/templates/pages/type_tmpl.js |
Exports type-page templates. |
assets/javascripts/templates/pages/settings_tmpl.js |
Exports settings templates. |
assets/javascripts/templates/pages/root_tmpl.js.erb |
Generates exported landing templates. |
assets/javascripts/templates/pages/root_tmpl.d.ts |
Declares generated landing exports. |
assets/javascripts/templates/pages/offline_tmpl.js |
Modularizes offline templates. |
assets/javascripts/templates/pages/news_tmpl.js.erb |
Generates news module exports and data. |
assets/javascripts/templates/pages/news_tmpl.d.ts |
Declares generated news exports. |
assets/javascripts/templates/pages/help_tmpl.js |
Exports help templates. |
assets/javascripts/templates/pages/about_tmpl.js |
Exports the about template. |
assets/javascripts/templates/notif_tmpl.js |
Exports notification templates. |
assets/javascripts/templates/notice_tmpl.js |
Exports persistent-notice templates. |
assets/javascripts/templates/error_tmpl.js |
Exports error templates. |
assets/javascripts/templates/base.js |
Builds the module-based template registry. |
assets/javascripts/models/type.js |
Exports the type model. |
assets/javascripts/models/model.js |
Exports the base model. |
assets/javascripts/models/entry.js |
Modularizes entry dependencies. |
assets/javascripts/models/doc.js |
Modularizes documentation models. |
assets/javascripts/lib/util.js |
Exports DOM utility functions. |
assets/javascripts/lib/page.js |
Exports router utilities and context. |
assets/javascripts/lib/local_storage_store.js |
Exports local-storage support. |
assets/javascripts/lib/license.js |
Removes the standalone license module. |
assets/javascripts/lib/favicon.js |
Exports favicon helpers. |
assets/javascripts/lib/events.js |
Exports the event base class. |
assets/javascripts/lib/cookies_store.js |
Exports cookie storage. |
assets/javascripts/lib/ajax.js |
Exports the AJAX helper. |
assets/javascripts/globals.d.ts |
Reworks ambient declarations for modules. |
assets/javascripts/docs.js.erb |
Imports the app before setting its catalog. |
assets/javascripts/docs.d.ts |
Declares the generated catalog module. |
assets/javascripts/debug.js |
Patches imported classes for instrumentation. |
assets/javascripts/collections/types.js |
Exports typed type collections. |
assets/javascripts/collections/entries.js |
Exports typed entry collections. |
assets/javascripts/collections/docs.js |
Exports documentation collections. |
assets/javascripts/collections/collection.js |
Exports the generic collection base. |
assets/javascripts/application.js |
Becomes the module boot entry point. |
assets/javascripts/app/update_checker.js |
Exports update checking. |
assets/javascripts/app/shortcuts.js |
Exports keyboard shortcuts. |
assets/javascripts/app/settings.js |
Exports application settings. |
assets/javascripts/app/serviceworker.js |
Exports service-worker management. |
assets/javascripts/app/searcher.js |
Exports asynchronous and synchronous searchers. |
assets/javascripts/app/router.js |
Exports the application router. |
assets/javascripts/app/offline_backup.js |
Exports offline backup support. |
assets/javascripts/app/db.js |
Exports database management. |
assets/javascripts/app/config.js.erb |
Generates exported configuration. |
assets/javascripts/app/config.d.ts |
Declares generated configuration types. |
assets/javascripts/app/app.js |
Replaces registries with direct imports. |
Review details
- Files reviewed: 98/104 changed files
- Comments generated: 3
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| //= depend_on news.json | ||
|
|
||
| app.templates.newsPage = () => ` <h1 class="_lined-heading">Changelog</h1> | ||
| import { app } from "../../app/app.js"; |
| ahead of the module graph. The import map rewrites each module's relative | ||
| imports onto its digested URL; it has to come before any module loads. %> | ||
| <%= javascript_tag 'vendor' %> | ||
| <script type="importmap" nonce="devdocs"><%= import_map_json %></script> |
| <%# Single-doc pages read their one doc off the body, so they skip docs.js | ||
| and the whole catalog with it. %> | ||
| <%= javascript_tag 'vendor' %> | ||
| <script type="importmap" nonce="devdocs"><%= import_map_json %></script> |
news_tmpl.js is a dependency of the app singleton, so it evaluated before `app` was initialised and threw on every load. Export the data from the leaf module instead. The graph test swaps this module for a side-effect-free fixture and so could never have failed. Assert the property directly.
Such a browser resolves every relative import against the undigested path, 404s on all of them, and sits on the loading screen. The in-app check cannot report that from inside the graph that failed to load, so test for import maps from a classic script that runs first. The documented baseline was already wrong: the assets target ES2022.
Concatenated scripts run sloppy, so a plain function's `this` was the global object. page.js cached the canonical link element there and favicon.js used it to reach resetFavicon. Modules are strict, and the first of those threw on the boot path. noImplicitThis catches the favicon shape. It does not catch page.js, where a cast asserting `this` had a type silenced the check.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sprockets has no notion of
import/export— its whole JS pipeline isconcat_javascript_sources, which appends each file and pokes a;between them. The single global scope that produced is what the
appobject was a registry for. Serving the modules individually instead
costs ~33 KB of cross-file compression and six levels of import depth,
which
modulepreloadflattens to about one round trip.Every module is served at a content-digested URL,
immutableandcacheable forever, and the import map that points at those URLs ships
inside the HTML, which is never cached. A client therefore reads one
build's map and fetches that build's modules, and can't end up running
half of one build and half of another. Old digests are kept for a couple
of builds so a deploy doesn't strand a client mid-load.
The modules import each other by relative path rather than by a bare
specifier so that tsc, editors and Node resolve them with no extra
configuration; the digests still apply because an import map may key on
a URL as well as on a bare name.
appkeeps only the state the rest of the app reads off it. The fourclass registries are gone, and with them the reason for most of
globals.d.ts. Two things had to move rather than be translated:
configbecomes its own leaf module. Five classes read it from astaticfield, which runs at class-definition time and would findappstill uninitialised inside the app/views import cycle. Thesame hazard made
static model = Docin the collections throw, sothe model is resolved from a method now.
The injection-error check compared
window.$against the app's owncopy, to notice extensions replacing the globals. Module scope makes
that impossible, which also makes the comparison always true — it
would have reported every error as an injection error and shown an
alert. Removed along with the fields that existed to feed it.
The vendored libraries assign globals rather than exporting, so they
stay one concatenated classic script loaded ahead of the graph. docs.js
and debug.js stay separate entries: single-doc pages skip the catalog,
and debug has to run before the boot it wraps.
The tests import the modules directly instead of concatenating files
into a
vmcontext, with a loader hook standing in for the modules ERBgenerates at build time. A new test evaluates the whole graph in the
order the browser does, which is the only thing that catches the
initialisation-cycle bugs above.