diff --git a/reviewboard/static/rb/js/extensions/index.ts b/reviewboard/static/rb/js/extensions/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8786a840225a82a7e9f785b6af5378edc7a4c586
--- /dev/null
+++ b/reviewboard/static/rb/js/extensions/index.ts
@@ -0,0 +1,9 @@
+/**
+ * All exports for Review Board's JavaScript extension support.
+ *
+ * Version Added:
+ *     7.1
+ */
+
+export * from './models';
+export * from './views';
diff --git a/reviewboard/static/rb/js/extensions/models/aliases.es6.js b/reviewboard/static/rb/js/extensions/models/aliases.es6.js
deleted file mode 100644
index b0b1fa21f2d7f6db64fda8c2a2865248b7b8e8c8..0000000000000000000000000000000000000000
--- a/reviewboard/static/rb/js/extensions/models/aliases.es6.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- * Alias the Djblets extension framework into the RB namespace.
- *
- * This provides a nice consistency, so that extensions don't have to refer to
- * Djblets. It also makes it easier down the road for us to subclass and
- * provide new functionality for these classes without extensions having to
- * update to use the new names.
- */
-RB.Extension = Djblets.Extension;
-RB.ExtensionHook = Djblets.ExtensionHook;
-RB.ExtensionHookPoint = Djblets.ExtensionHookPoint;
diff --git a/reviewboard/static/rb/js/extensions/models/commentDialogHookModel.es6.js b/reviewboard/static/rb/js/extensions/models/commentDialogHookModel.es6.js
deleted file mode 100644
index acc4d92c0e6eb3d105b8bd30497a06f8f55d5bad..0000000000000000000000000000000000000000
--- a/reviewboard/static/rb/js/extensions/models/commentDialogHookModel.es6.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Provides extensibility for the Comment Dialog.
- *
- * Users of this hook can provide a Backbone View that will have access to
- * the CommentDialog and its CommentEditor (through the commentDialog and
- * commentEditor options passed to the view). They can call public API on
- * the comment dialog and augment the contents of the dialog.
- */
-RB.CommentDialogHook = RB.ExtensionHook.extend({
-    hookPoint: new RB.ExtensionHookPoint(),
-
-    defaults: _.defaults({
-        viewType: null,
-    }, RB.ExtensionHook.prototype.defaults),
-
-    /**
-     * Set up the hook.
-     */
-    setUpHook() {
-        console.assert(this.get('viewType'),
-                       'CommentDialogHook instance does not have a ' +
-                       '"viewType" attribute set.');
-    },
-});
diff --git a/reviewboard/static/rb/js/extensions/models/commentDialogHookModel.ts b/reviewboard/static/rb/js/extensions/models/commentDialogHookModel.ts
new file mode 100644
index 0000000000000000000000000000000000000000..87b7b3fe2429dbe04bb9f373425c944c282a66a6
--- /dev/null
+++ b/reviewboard/static/rb/js/extensions/models/commentDialogHookModel.ts
@@ -0,0 +1,66 @@
+/**
+ * Extension hook for extending the comment dialog.
+ *
+ * Version Changed:
+ *     7.1:
+ *     This is now an ESM module supporting TypeScript.
+ */
+
+import { spina } from '@beanbag/spina';
+
+import {
+    type ExtensionHookAttrs,
+    ExtensionHook,
+} from './extensionHookModel';
+import {
+    ExtensionHookPoint,
+} from './extensionHookPointModel';
+import {
+    type BaseCommentDialogHookViewClass,
+} from '../views/baseCommentDialogHookView';
+
+
+/**
+ * Attributes for CommentDialogHook.
+ *
+ * Version Added:
+ *     7.1
+ */
+export interface CommentDialogHookAttrs extends ExtensionHookAttrs {
+    /** The view type to construct in the comment dialog. */
+    viewType: BaseCommentDialogHookViewClass;
+}
+
+
+/**
+ * Provides extensibility for the Comment Dialog.
+ *
+ * Users of this hook can provide a Backbone View that will have access to
+ * the CommentDialog and its CommentEditor (through the commentDialog and
+ * commentEditor options passed to the view). They can call public API on
+ * the comment dialog and augment the contents of the dialog.
+ *
+ * Version Changed:
+ *     7.1:
+ *     This is now a modern ES6-style class and supports typing using
+ *     TypeScript.
+ */
+@spina
+export class CommentDialogHook<
+    TAttrs extends CommentDialogHookAttrs = CommentDialogHookAttrs,
+> extends ExtensionHook<TAttrs> {
+    static hookPoint = new ExtensionHookPoint();
+
+    static defaults: Partial<CommentDialogHookAttrs> = {
+        viewType: null,
+    };
+
+    /**
+     * Set up the hook.
+     */
+    setUpHook() {
+        console.assert(this.get('viewType'),
+                       'CommentDialogHook instance does not have a ' +
+                       '"viewType" attribute set.');
+    }
+}
diff --git a/reviewboard/static/rb/js/extensions/models/extensionHookModel.ts b/reviewboard/static/rb/js/extensions/models/extensionHookModel.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6fe055b1d1c039c0c575909be43b095720108609
--- /dev/null
+++ b/reviewboard/static/rb/js/extensions/models/extensionHookModel.ts
@@ -0,0 +1,14 @@
+/**
+ * Base support for building extension hooks.
+ *
+ * This is a re-export of the base extension hook infrastructure found in
+ * Djblets.
+ *
+ * Version Added:
+ *     7.1
+ */
+
+export {
+    type ExtensionHookAttrs,
+    ExtensionHook,
+} from 'djblets/extensions';
diff --git a/reviewboard/static/rb/js/extensions/models/extensionHookPointModel.ts b/reviewboard/static/rb/js/extensions/models/extensionHookPointModel.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d38b475f6a7e2c806566b63cd97692038176a6ca
--- /dev/null
+++ b/reviewboard/static/rb/js/extensions/models/extensionHookPointModel.ts
@@ -0,0 +1,11 @@
+/**
+ * Base support for building extension hook points.
+ *
+ * This is a re-export of the base extension hook point infrastructure found in
+ * Djblets.
+ *
+ * Version Added:
+ *     7.1
+ */
+
+export { ExtensionHookPoint } from 'djblets/extensions';
diff --git a/reviewboard/static/rb/js/extensions/models/extensionModel.ts b/reviewboard/static/rb/js/extensions/models/extensionModel.ts
new file mode 100644
index 0000000000000000000000000000000000000000..91126d08587568b07465adde68cbe522d314ee3d
--- /dev/null
+++ b/reviewboard/static/rb/js/extensions/models/extensionModel.ts
@@ -0,0 +1,13 @@
+/**
+ * Base support for building extensions.
+ *
+ * This is a re-export of the base extension infrastructure found in Djblets.
+ *
+ * Version Added:
+ *     7.1
+ */
+
+export {
+    type BaseExtensionAttrs,
+    Extension,
+} from 'djblets/extensions';
diff --git a/reviewboard/static/rb/js/extensions/models/fileAttachmentThumbnailContainerHookModel.es6.js b/reviewboard/static/rb/js/extensions/models/fileAttachmentThumbnailContainerHookModel.es6.js
deleted file mode 100644
index 99bba4eed9f4ac508cefde31981af8937db4f1cb..0000000000000000000000000000000000000000
--- a/reviewboard/static/rb/js/extensions/models/fileAttachmentThumbnailContainerHookModel.es6.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Provides extensibility for File Attachment thumbnails.
- *
- * This can be used to display additional UI on file attachment containers
- * and on the file attachment actions menu. This should not be used to create
- * or modify the thumbnail image itself, use :py:class:`~reviewboard
- * .extensions.hooks.FileAttachmentThumbnailHook` for that behavior instead.
- *
- * Users of this hook must provide a Backbone View (not an instance) which
- * will modify the File Attachment thumbnail. The view will have access to
- * the FileAttachmentThumbnailView and its FileAttachment model (through
- * the thumbnailView and fileAttachment options passed to the view).
- *
- * Model Attributes:
- *     viewType (Backbone.View):
- *         The view type (not an instance) which will modify the File
- *         Attachment thumbnail.
- *
- * Version Added:
- *     6.0
- */
-RB.FileAttachmentThumbnailContainerHook = RB.ExtensionHook.extend({
-    hookPoint: new RB.ExtensionHookPoint(),
-
-    defaults: _.defaults({
-        viewType: null,
-    }, RB.ExtensionHook.prototype.defaults),
-
-    /**
-     * Set up the hook.
-     */
-    setUpHook() {
-        console.assert(this.get('viewType'),
-                       'FileAttachmentThumbnailContainerHook instance does ' +
-                       'not have a "viewType" attribute set.');
-    },
-});
diff --git a/reviewboard/static/rb/js/extensions/models/fileAttachmentThumbnailContainerHookModel.ts b/reviewboard/static/rb/js/extensions/models/fileAttachmentThumbnailContainerHookModel.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b1755ed579691eb1de8038906a6a00388b21159c
--- /dev/null
+++ b/reviewboard/static/rb/js/extensions/models/fileAttachmentThumbnailContainerHookModel.ts
@@ -0,0 +1,74 @@
+/**
+ * Extension hook for extending file attachment thumbnails.
+ *
+ * Version Changed:
+ *     7.1:
+ *     This is now an ESM module supporting TypeScript.
+ */
+
+import { spina } from '@beanbag/spina';
+
+import {
+    type ExtensionHookAttrs,
+    ExtensionHook,
+} from './extensionHookModel';
+import { ExtensionHookPoint } from './extensionHookPointModel';
+import {
+    type BaseFileAttachmentThumbnailContainerHookViewClass,
+} from '../views/baseFileAttachmentThumbnailContainerHookView';
+
+
+/**
+ * Attributes for FileAttachmentThumbnailContainerHook.
+ *
+ * Version Added:
+ *     7.1
+ */
+export interface FileAttachmentThumbnailContainerHookAttrs
+extends ExtensionHookAttrs {
+    /** The view type to construct to modify the thumbnail view. */
+    viewType: BaseFileAttachmentThumbnailContainerHookViewClass;
+}
+
+
+/**
+ * Provides extensibility for File Attachment thumbnails.
+ *
+ * This can be used to display additional UI on file attachment containers
+ * and on the file attachment actions menu. This should not be used to create
+ * or modify the thumbnail image itself, use :py:class:`~reviewboard
+ * .extensions.hooks.FileAttachmentThumbnailHook` for that behavior instead.
+ *
+ * Users of this hook must provide a Backbone View (not an instance) which
+ * will modify the File Attachment thumbnail. The view will have access to
+ * the FileAttachmentThumbnailView and its FileAttachment model (through
+ * the thumbnailView and fileAttachment options passed to the view).
+ *
+ * Version Changed:
+ *     7.1:
+ *     This is now a modern ES6-style class and supports typing using
+ *     TypeScript.
+ *
+ * Version Added:
+ *     6.0
+ */
+@spina
+export class FileAttachmentThumbnailContainerHook<
+    TAttrs extends FileAttachmentThumbnailContainerHookAttrs =
+        FileAttachmentThumbnailContainerHookAttrs,
+> extends ExtensionHook<TAttrs> {
+    static hookPoint = new ExtensionHookPoint();
+
+    static defaults: Partial<FileAttachmentThumbnailContainerHookAttrs> = {
+        viewType: null,
+    };
+
+    /**
+     * Set up the hook.
+     */
+    setUpHook() {
+        console.assert(this.get('viewType'),
+                       'FileAttachmentThumbnailContainerHook instance does ' +
+                       'not have a "viewType" attribute set.');
+    }
+}
diff --git a/reviewboard/static/rb/js/extensions/models/index.ts b/reviewboard/static/rb/js/extensions/models/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..17d9cc8e9be2e515f9c37634aaeae667fb0d825d
--- /dev/null
+++ b/reviewboard/static/rb/js/extensions/models/index.ts
@@ -0,0 +1,15 @@
+/**
+ * All model exports for Review Board's JavaScript extension support.
+ *
+ * Version Added:
+ *     7.1
+ */
+
+export * from './commentDialogHookModel';
+export * from './extensionHookModel';
+export * from './extensionHookPointModel';
+export * from './extensionModel';
+export * from './fileAttachmentThumbnailContainerHookModel';
+export * from './reviewDialogCommentHookModel';
+export * from './reviewDialogHookModel';
+export * from './reviewRequestActionHookModel';
diff --git a/reviewboard/static/rb/js/extensions/models/reviewDialogCommentHookModel.es6.js b/reviewboard/static/rb/js/extensions/models/reviewDialogCommentHookModel.es6.js
deleted file mode 100644
index 3c39d3825e8ca75adeb9d227d2472279f5f7ced8..0000000000000000000000000000000000000000
--- a/reviewboard/static/rb/js/extensions/models/reviewDialogCommentHookModel.es6.js
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Adds additional rendering or UI for a comment in the review dialog.
- *
- * This can be used to display additional UI and even additional fields in
- * the review dialog, which can reflect and potentially modify extra data
- * for a comment.
- *
- * A Backbone View type (not an instance) must be provided for the viewType
- * attribute. When rendering comments in the dialog, an instance of the
- * provided view will be created and passed the comment as the view's model.
- */
-RB.ReviewDialogCommentHook = RB.ExtensionHook.extend({
-    hookPoint: new RB.ExtensionHookPoint(),
-
-    defaults: _.defaults({
-        viewType: null,
-    }, RB.ExtensionHook.prototype.defaults),
-
-    /**
-     * Set up the hook.
-     */
-    setUpHook() {
-        console.assert(this.get('viewType'),
-                       'ReviewDialogCommentHook instance does not have a ' +
-                       '"viewType" attribute set.');
-    },
-});
diff --git a/reviewboard/static/rb/js/extensions/models/reviewDialogCommentHookModel.ts b/reviewboard/static/rb/js/extensions/models/reviewDialogCommentHookModel.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e3187a4dd8d58aa888ac06595d1f766f17afcb02
--- /dev/null
+++ b/reviewboard/static/rb/js/extensions/models/reviewDialogCommentHookModel.ts
@@ -0,0 +1,67 @@
+/**
+ * Extension hook for extending a comment in the Review Dialog.
+ *
+ * Version Changed:
+ *     7.1:
+ *     This is now an ESM module supporting TypeScript.
+ */
+
+import { spina } from '@beanbag/spina';
+
+import {
+    type ExtensionHookAttrs,
+    ExtensionHook,
+} from './extensionHookModel';
+import { ExtensionHookPoint } from './extensionHookPointModel';
+import {
+    type BaseReviewDialogCommentHookViewClass,
+} from '../views/baseReviewDialogCommentHookView';
+
+
+/**
+ * Attributes for ReviewDialogCommentHook.
+ *
+ * Version Added:
+ *     7.1
+ */
+export interface ReviewDialogCommentHookAttrs extends ExtensionHookAttrs {
+    /** The view type to construct in the comment section. */
+    viewType: BaseReviewDialogCommentHookViewClass;
+}
+
+
+/**
+ * Adds additional rendering or UI for a comment in the review dialog.
+ *
+ * This can be used to display additional UI and even additional fields in
+ * the review dialog, which can reflect and potentially modify extra data
+ * for a comment.
+ *
+ * A View type (not an instance) must be provided for the viewType
+ * attribute. When rendering comments in the dialog, an instance of the
+ * provided view will be created and passed the comment as the view's model.
+ *
+ * Version Changed:
+ *     7.1:
+ *     This is now a modern ES6-style class and supports typing using
+ *     TypeScript.
+ */
+@spina
+export class ReviewDialogCommentHook<
+    TAttrs extends ReviewDialogCommentHookAttrs = ReviewDialogCommentHookAttrs,
+> extends ExtensionHook<TAttrs> {
+    static hookPoint = new ExtensionHookPoint();
+
+    static defaults: Partial<ReviewDialogCommentHookAttrs> = {
+        viewType: null,
+    };
+
+    /**
+     * Set up the hook.
+     */
+    setUpHook() {
+        console.assert(this.get('viewType'),
+                       'ReviewDialogCommentHook instance does not have a ' +
+                       '"viewType" attribute set.');
+    }
+}
diff --git a/reviewboard/static/rb/js/extensions/models/reviewDialogHookModel.es6.js b/reviewboard/static/rb/js/extensions/models/reviewDialogHookModel.es6.js
deleted file mode 100644
index 0d3c963e9b66a8533c89e7a65c704041080811a3..0000000000000000000000000000000000000000
--- a/reviewboard/static/rb/js/extensions/models/reviewDialogHookModel.es6.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Adds additional rendering or UI to the top of the review dialog.
- *
- * This can be used to display additional UI and even additional fields in
- * the review dialog before all comments, below the Ship It checkbox.
- *
- * A Backbone View type (not an instance) must be provided for the viewType
- * attribute. When rendering comments in the dialog, an instance of the
- * provided view will be created and passed the comment as the view's model.
- */
-RB.ReviewDialogHook = RB.ExtensionHook.extend({
-    hookPoint: new RB.ExtensionHookPoint(),
-
-    defaults: _.defaults({
-        viewType: null,
-    }, RB.ExtensionHook.prototype.defaults),
-
-    /**
-     * Set up the hook.
-     */
-    setUpHook() {
-        console.assert(this.get('viewType'),
-                       'ReviewDialogHook instance does not have a ' +
-                       '"viewType" attribute set.');
-    },
-});
diff --git a/reviewboard/static/rb/js/extensions/models/reviewDialogHookModel.ts b/reviewboard/static/rb/js/extensions/models/reviewDialogHookModel.ts
new file mode 100644
index 0000000000000000000000000000000000000000..52303445a670d623546b48a3a148c9215dcad283
--- /dev/null
+++ b/reviewboard/static/rb/js/extensions/models/reviewDialogHookModel.ts
@@ -0,0 +1,66 @@
+/**
+ * Extension hook for extending the Review Dialog.
+ *
+ * Version Changed:
+ *     7.1:
+ *     This is now an ESM module supporting TypeScript.
+ */
+
+import { spina } from '@beanbag/spina';
+
+import {
+    type ExtensionHookAttrs,
+    ExtensionHook,
+} from './extensionHookModel';
+import { ExtensionHookPoint } from './extensionHookPointModel';
+import {
+    type BaseReviewDialogHookViewClass,
+} from '../views/baseReviewDialogHookView';
+
+
+/**
+ * Attributes for ReviewDialogHook.
+ *
+ * Version Added:
+ *     7.1
+ */
+export interface ReviewDialogHookAttrs extends ExtensionHookAttrs {
+    /** The view type to construct in the Review Dialog. */
+    viewType: BaseReviewDialogHookViewClass;
+}
+
+
+/**
+ * Adds additional rendering or UI to the top of the review dialog.
+ *
+ * This can be used to display additional UI and even additional fields in
+ * the review dialog before all comments, below the Ship It checkbox.
+ *
+ * A Backbone View type (not an instance) must be provided for the viewType
+ * attribute. When rendering comments in the dialog, an instance of the
+ * provided view will be created and passed the comment as the view's model.
+ *
+ * Version Changed:
+ *     7.1:
+ *     This is now a modern ES6-style class and supports typing using
+ *     TypeScript.
+ */
+@spina
+export class ReviewDialogHook<
+    TAttrs extends ReviewDialogHookAttrs = ReviewDialogHookAttrs,
+> extends ExtensionHook<TAttrs> {
+    static hookPoint = new ExtensionHookPoint();
+
+    static defaults: Partial<ReviewDialogHookAttrs> = {
+        viewType: null,
+    };
+
+    /**
+     * Set up the hook.
+     */
+    setUpHook() {
+        console.assert(this.get('viewType'),
+                       'ReviewDialogHook instance does not have a ' +
+                       '"viewType" attribute set.');
+    }
+}
diff --git a/reviewboard/static/rb/js/extensions/models/reviewRequestActionHookModel.es6.js b/reviewboard/static/rb/js/extensions/models/reviewRequestActionHookModel.es6.js
deleted file mode 100644
index 9452c194f384bf366ab55513362c7f19201b4ac0..0000000000000000000000000000000000000000
--- a/reviewboard/static/rb/js/extensions/models/reviewRequestActionHookModel.es6.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * A hook for providing callbacks for review request actions.
- *
- * Model Attributes:
- *     callbacks (object):
- *         An object that maps selectors to handlers. When setting up actions
- *         for review requests, the handler will be bound to the "click"
- *         JavaScript event. Defaults to null.
- *
- * Example:
- *     RBSample = {};
- *
- *     (function() {
- *         RBSample.Extension = RB.Extension.extend({
- *             initialize: function () {
- *                 var _onMyNewActionClicked;
- *
- *                 _super(this).initialize.call(this);
- *
- *                 _onMyNewActionClicked = function() {
- *                     if (confirm(gettext('Are you sure?'))) {
- *                         console.log('My new action confirmed! =]');
- *                     }
- *                     else {
- *                         console.log('My new action not confirmed! D=');
- *                     }
- *                 };
- *
- *                 new RB.ReviewRequestActionHook({
- *                     extension: this,
- *                     callbacks: {
- *                        '#my-new-action': _onMyNewActionClicked
- *                     }
- *                 });
- *             }
- *         });
- *     })();
- */
-RB.ReviewRequestActionHook = RB.ExtensionHook.extend({
-    hookPoint: new RB.ExtensionHookPoint(),
-
-    defaults: _.defaults({
-        callbacks: null,
-    }, RB.ExtensionHook.prototype.defaults),
-
-    /**
-     * Set up the extension hook.
-     */
-    setUpHook() {
-        console.assert(this.get('callbacks'),
-                       'ReviewRequestActionHook instance does not have a ' +
-                       '"callbacks" attribute set.');
-    },
-});
diff --git a/reviewboard/static/rb/js/extensions/models/reviewRequestActionHookModel.ts b/reviewboard/static/rb/js/extensions/models/reviewRequestActionHookModel.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3c4adb771c978078d73bb976d1e50552f5b7693d
--- /dev/null
+++ b/reviewboard/static/rb/js/extensions/models/reviewRequestActionHookModel.ts
@@ -0,0 +1,132 @@
+/**
+ * Extension hook for review request actions.
+ *
+ * Version Changed:
+ *     7.1:
+ *     This is now an ESM module supporting TypeScript.
+ */
+
+import {
+    spina,
+} from '@beanbag/spina';
+
+import {
+    type ExtensionHookAttrs,
+    ExtensionHook,
+} from './extensionHookModel';
+import {
+    ExtensionHookPoint,
+} from './extensionHookPointModel';
+
+
+/**
+ * Attributes for ReviewRequestActionHook.
+ *
+ * Version Added:
+ *     7.1
+ */
+export interface ReviewRequestActionHookAttrs extends ExtensionHookAttrs {
+    /*
+     * A mapping of selectors to handlers.
+     *
+     * When setting up actions for review requests, the handler will be
+     * bound to the "click" JavaScript event. It defaults to null.
+     */
+    callbacks: Record<
+        string,
+        (eventObject: JQuery.TriggeredEvent) => void
+    >;
+}
+
+
+/**
+ * A hook for providing callbacks for review request actions.
+ *
+ * Version Changed:
+ *     7.1:
+ *     This is now a modern ES6-style class and supports typing using
+ *     TypeScript.
+ *
+ * Examples:
+ *     .. code-block:: typescript
+ *        :caption: TypeScript
+ *
+ *         import { spina } from '@beanbag/spina';
+ *         import {
+ *             Extension,
+ *             ReviewRequestActionHook,
+ *         } from '@beanbag/reviewboard/extensions';
+ *
+ *
+ *         @spina
+ *         export class RBSampleExtension extends Extension {
+ *             initialize() {
+ *                 super.initialize();
+ *
+ *                 const _onMyNewActionClicked = () => {
+ *                 };
+ *
+ *                 new ReviewRequestActionHook({
+ *                     extension: this,
+ *                     callbacks: {
+ *                         '#my-new-action': () => {
+ *                             if (confirm(gettext('Are you sure?'))) {
+ *                                 console.log('Confirmed!');
+ *                             }
+ *                             else {
+ *                                 console.log('Not confirmed...');
+ *                             }
+ *                         },
+ *                     },
+ *                 });
+ *             }
+ *         }
+ *
+ *     .. code-block:: javascript
+ *        :caption: JavaScript
+ *
+ *         RBSample = {};
+ *
+ *         RBSample.Extension = RB.Extension.extend({
+ *             initialize: function () {
+ *                 var _onMyNewActionClicked;
+ *
+ *                 _super(this).initialize.call(this);
+ *
+ *                 _onMyNewActionClicked = function() {
+ *                     if (confirm(gettext('Are you sure?'))) {
+ *                         console.log('Confirmed!');
+ *                     }
+ *                     else {
+ *                         console.log('Not confirmed...');
+ *                     }
+ *                 };
+ *
+ *                 new RB.ReviewRequestActionHook({
+ *                     extension: this,
+ *                     callbacks: {
+ *                        '#my-new-action': _onMyNewActionClicked
+ *                     }
+ *                 });
+ *             }
+ *         });
+ */
+@spina
+export class ReviewRequestActionHook<
+    TAttrs extends ReviewRequestActionHookAttrs = ReviewRequestActionHookAttrs,
+> extends ExtensionHook<TAttrs> {
+    static hookPoint = new ExtensionHookPoint();
+
+    static defaults: Partial<ReviewRequestActionHookAttrs> = {
+        callbacks: null,
+    };
+
+    /**
+     * Set up the extension hook.
+     */
+    setUpHook() {
+        console.assert(this.get('callbacks'),
+                       'ReviewRequestActionHook instance does not have a ' +
+                       '"callbacks" attribute set.');
+    }
+}
diff --git a/reviewboard/static/rb/js/extensions/views/baseFileAttachmentThumbnailContainerHookView.ts b/reviewboard/static/rb/js/extensions/views/baseFileAttachmentThumbnailContainerHookView.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b3cbe760b65851beeadc57bea88c00a3f0ee6e05
--- /dev/null
+++ b/reviewboard/static/rb/js/extensions/views/baseFileAttachmentThumbnailContainerHookView.ts
@@ -0,0 +1,101 @@
+/**
+ * Base support for custom Review Dialog hook views.
+ *
+ * Version Added:
+ *     7.1
+ */
+
+import * as Backbone from 'backbone';
+import {
+    type BaseModel,
+    BaseView,
+    spina,
+} from '@beanbag/spina';
+
+import { type FileAttachment } from 'reviewboard/common';
+import { type FileAttachmentThumbnailView } from 'reviewboard/reviews';
+import { type Extension } from '../models/extensionModel';
+
+
+/**
+ * Options for BaseFileAttachmentThumbnailContainerHookView.
+ *
+ * Version Added:
+ *     7.1
+ */
+export interface FileAttachmentThumbnailContainerHookViewOptions
+extends Backbone.ViewOptions {
+    /** The extension that owns the hook. */
+    extension: Extension;
+
+    /** The file attachment being rendered. */
+    fileAttachment: FileAttachment;
+
+    /** The thumbnail view managing the thumbnail. */
+    thumbnailView: FileAttachmentThumbnailView;
+}
+
+
+/**
+ * A base view for rendering into the Review Dialog.
+ *
+ * This is intended to be subclassed and passed in a view type to
+ * :js:class:`RB.ReviewDialogViewHook`. It takes in the parent extension
+ * and can then render into the dialog or hook into behavior.
+ *
+ * Version Added:
+ *     7.1
+ */
+@spina
+export class BaseFileAttachmentThumbnailContainerHookView extends BaseView<
+    BaseModel,
+    HTMLDivElement,
+    unknown,
+    FileAttachmentThumbnailContainerHookViewOptions
+> {
+    /**********************
+     * Instance variables *
+     **********************/
+
+    /** The extension that owns the hook. */
+    extension: Extension;
+
+    /** The file attachment being rendered. */
+    fileAttachment: FileAttachment;
+
+    /** The thumbnail view managing the thumbnail. */
+    thumbnailView: FileAttachmentThumbnailView;
+
+    /**
+     * Pre-initialize the view.
+     *
+     * This will set the instance attributes based on the options provided
+     * before the subclass's initialization code runs.
+     *
+     * Args:
+     *     options (FileAttachmentThumbnailContainerHookViewOptions):
+     *         The options for the view.
+     */
+    preinitialize(
+        options: Partial<FileAttachmentThumbnailContainerHookViewOptions>,
+    ) {
+        this.extension = options.extension;
+        this.fileAttachment = options.fileAttachment;
+        this.thumbnailView = options.thumbnailView;
+    }
+}
+
+
+/**
+ * A type representing a BaseFileAttachmentThumbnailContainerHookView class.
+ *
+ * This types the constructor of the class such that it will properly return
+ * an instance that is typed with the right constructor arguments and default
+ * generics.
+ *
+ * Version Added:
+ *     7.1
+ */
+export type BaseFileAttachmentThumbnailContainerHookViewClass =
+    new (options: FileAttachmentThumbnailContainerHookViewOptions) =>
+    BaseFileAttachmentThumbnailContainerHookView;
diff --git a/reviewboard/static/rb/js/extensions/views/index.ts b/reviewboard/static/rb/js/extensions/views/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f306a31bbd331028d8e2a18ffc0a82d424679ce2
--- /dev/null
+++ b/reviewboard/static/rb/js/extensions/views/index.ts
@@ -0,0 +1,11 @@
+/**
+ * All view exports for Review Board's JavaScript extension support.
+ *
+ * Version Added:
+ *     7.1
+ */
+
+export * from './baseCommentDialogHookView';
+export * from './baseFileAttachmentThumbnailContainerHookView';
+export * from './baseReviewDialogCommentHookView';
+export * from './baseReviewDialogHookView';
diff --git a/reviewboard/static/rb/js/reviews/views/commentDialogView.ts b/reviewboard/static/rb/js/reviews/views/commentDialogView.ts
index eecbc3295197e8557caa7567aec6ead4c26bc63c..5f5e9c718d7ec8ea0f69098e4fcca6739c28dff8 100644
--- a/reviewboard/static/rb/js/reviews/views/commentDialogView.ts
+++ b/reviewboard/static/rb/js/reviews/views/commentDialogView.ts
@@ -12,6 +12,7 @@ import {
     EnabledFeatures,
     UserSession,
 } from 'reviewboard/common';
+import { CommentDialogHook } from 'reviewboard/extensions';
 import { TextEditorView } from 'reviewboard/ui';
 
 import { type SerializedComment } from '../models/commentData';
@@ -682,7 +683,7 @@ export class CommentDialogView extends BaseView<
         this.#onPublishedCommentsChanged();
 
         /* Add any hooks. */
-        RB.CommentDialogHook.each(hook => {
+        CommentDialogHook.each(hook => {
             const HookViewType = hook.get('viewType');
             const hookView = new HookViewType({
                 commentDialog: this,
diff --git a/reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts b/reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts
index fe1be683240870ccbdc703b66b2a1466ff0d9df7..740b1dafb60de16ac1ade3ba80b6f40da641df11 100644
--- a/reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts
+++ b/reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts
@@ -13,6 +13,9 @@ import {
     type ReviewRequest,
     FileAttachmentStates,
 } from 'reviewboard/common';
+import {
+    FileAttachmentThumbnailContainerHook,
+} from 'reviewboard/extensions';
 import {
     FieldStateLabelThemes,
     FieldStateLabelView,
@@ -359,7 +362,7 @@ export class FileAttachmentThumbnailView extends BaseView<
              * Add any hooks. If renderThumbnail is true then the hooks will
              * have already been added.
             */
-            RB.FileAttachmentThumbnailContainerHook.each(hook => {
+            FileAttachmentThumbnailContainerHook.each(hook => {
                 const HookViewType = hook.get('viewType');
                 const hookView = new HookViewType({
                     el: this.el,
diff --git a/reviewboard/static/rb/js/reviews/views/reviewDialogView.ts b/reviewboard/static/rb/js/reviews/views/reviewDialogView.ts
index da18d661b5920c59cca1fe2cdc822e9655a957f0..3e892fc171e8277d7928389667d6451275626ff2 100644
--- a/reviewboard/static/rb/js/reviews/views/reviewDialogView.ts
+++ b/reviewboard/static/rb/js/reviews/views/reviewDialogView.ts
@@ -17,6 +17,10 @@ import {
     ResourceCollection,
     UserSession,
 } from 'reviewboard/common';
+import {
+    ReviewDialogCommentHook,
+    ReviewDialogHook,
+} from 'reviewboard/extensions';
 import {
     type TextEditorView,
     RichTextInlineEditorView,
@@ -289,7 +293,7 @@ class BaseCommentView<
         this.listenTo(this.model, 'destroying',
                       () => this.stopListening(this.model));
 
-        RB.ReviewDialogCommentHook.each(hook => {
+        ReviewDialogCommentHook.each(hook => {
             const HookView = hook.get('viewType');
             const hookView = new HookView({
                 extension: hook.get('extension'),
@@ -1407,7 +1411,7 @@ export class ReviewDialogView extends BaseView<
         this.#updateTipsVisibility(
             userSession.get('showReviewDialogTips'));
 
-        RB.ReviewDialogHook.each(hook => {
+        ReviewDialogHook.each(hook => {
             const HookView = hook.get('viewType');
             const hookView = new HookView({
                 extension: hook.get('extension'),
diff --git a/reviewboard/static/rb/js/reviews/views/reviewRequestEditorView.ts b/reviewboard/static/rb/js/reviews/views/reviewRequestEditorView.ts
index fa3396f4896044ba9d42c9d197a2f72153d37597..5692b80aab3f97dcc396d0ef79a19e614760e259 100644
--- a/reviewboard/static/rb/js/reviews/views/reviewRequestEditorView.ts
+++ b/reviewboard/static/rb/js/reviews/views/reviewRequestEditorView.ts
@@ -12,6 +12,7 @@ import {
     ReviewRequest,
     UserSession,
 } from 'reviewboard/common';
+import { ReviewRequestActionHook } from 'reviewboard/extensions';
 import { DnDUploader } from 'reviewboard/ui';
 
 import { type ReviewRequestEditor } from '../models/reviewRequestEditorModel';
@@ -847,7 +848,7 @@ export class ReviewRequestEditorView extends BaseView<
      * Set up all review request actions and listens for events.
      */
     _setupActions() {
-        RB.ReviewRequestActionHook.each(hook => {
+        ReviewRequestActionHook.each(hook => {
             _.each(hook.get('callbacks'),
                    (handler, selector) => this.$(selector).click(handler));
         });
diff --git a/reviewboard/staticbundles.py b/reviewboard/staticbundles.py
index 95fbe2a45be0e93412435bacb235efa8f8e9a447..502275e28b6d462303178861f7ece5b42c34f2a0 100644
--- a/reviewboard/staticbundles.py
+++ b/reviewboard/staticbundles.py
@@ -107,7 +107,12 @@ PIPELINE_JAVASCRIPT = {
     },
     'common': {
         'source_filenames': (
+            # Include the common ESM bundle and the extensions bundle. This
+            # will ensure they're both exposed as "RB", but allow
+            # `reviewboard/extensions` to be a separate ESM bundle that
+            # extension code can target.
             'rb/js/common/index.ts',
+            'rb/js/extensions/index.ts',
 
             # Legacy JavaScript
             'rb/js/utils/underscoreUtils.es6.js',
@@ -120,7 +125,6 @@ PIPELINE_JAVASCRIPT = {
             'rb/js/utils/keyBindingUtils.es6.js',
             'rb/js/utils/urlUtils.es6.js',
             'rb/js/collections/filteredCollection.es6.js',
-            'rb/js/extensions/models/aliases.es6.js',
             'rb/js/extensions/models/commentDialogHookModel.es6.js',
             'rb/js/extensions/models/fileAttachmentThumbnailContainerHookModel.es6.js',
             'rb/js/extensions/models/reviewDialogCommentHookModel.es6.js',
