Bug 1541631 - Part 3: Merge 'content' onto the source itself and rely on query caching. r=jlast
authorLogan Smyth <loganfsmyth@gmail.com>
Fri, 16 Aug 2019 00:25:56 +0000
changeset 488390 4600340598aa1823ef007a8a6610404ecfac0c4b
parent 488389 98dd6077c4d543f87e1e0841a430747b1c9913ac
child 488391 c3131097b7c34fdb8606bab7d6d4972dca632249
push id36443
push userccoroiu@mozilla.com
push dateFri, 16 Aug 2019 09:48:15 +0000
treeherdermozilla-central@5d4cbfe103bb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjlast
bugs1541631
milestone70.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1541631 - Part 3: Merge 'content' onto the source itself and rely on query caching. r=jlast Differential Revision: https://phabricator.services.mozilla.com/D42028
devtools/client/debugger/src/actions/ast/setInScopeLines.js
devtools/client/debugger/src/actions/file-search.js
devtools/client/debugger/src/components/Editor/EditorMenu.js
devtools/client/debugger/src/components/Editor/Footer.js
devtools/client/debugger/src/components/Editor/HighlightLine.js
devtools/client/debugger/src/components/Editor/index.js
devtools/client/debugger/src/components/Editor/menus/editor.js
devtools/client/debugger/src/components/Editor/tests/DebugLine.spec.js
devtools/client/debugger/src/components/Editor/tests/Editor.spec.js
devtools/client/debugger/src/components/Editor/tests/Footer.spec.js
devtools/client/debugger/src/components/PrimaryPanes/Outline.js
devtools/client/debugger/src/reducers/sources.js
devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js
devtools/client/debugger/src/types.js
devtools/client/debugger/src/utils/breakpoint/tests/astBreakpointLocation.spec.js
devtools/client/debugger/src/utils/editor/tests/editor.spec.js
devtools/client/debugger/src/utils/function.js
devtools/client/debugger/src/utils/isMinified.js
devtools/client/debugger/src/utils/test-mockup.js
devtools/client/debugger/src/utils/tests/ast.spec.js
devtools/client/debugger/src/utils/tests/function.spec.js
devtools/client/debugger/src/utils/tests/source.spec.js
devtools/client/debugger/src/utils/tests/wasm.spec.js
devtools/client/debugger/src/workers/parser/tests/findOutOfScopeLocations.spec.js
devtools/client/debugger/src/workers/parser/tests/framework.spec.js
devtools/client/debugger/src/workers/parser/tests/getScopes.spec.js
devtools/client/debugger/src/workers/parser/tests/getSymbols.spec.js
devtools/client/debugger/src/workers/parser/tests/helpers/index.js
devtools/client/debugger/src/workers/parser/tests/steps.spec.js
devtools/client/debugger/src/workers/parser/utils/tests/ast.spec.js
devtools/client/debugger/test/mochitest/helpers.js
testing/talos/talos/tests/devtools/addon/content/tests/debugger/debugger-helpers.js
--- a/devtools/client/debugger/src/actions/ast/setInScopeLines.js
+++ b/devtools/client/debugger/src/actions/ast/setInScopeLines.js
@@ -27,32 +27,31 @@ function getOutOfScopeLines(outOfScopeLo
   return uniq(
     flatMap(outOfScopeLocations, location =>
       range(location.start.line, location.end.line)
     )
   );
 }
 
 async function getInScopeLines(cx, location, { dispatch, getState, parser }) {
-  const { source, content } = getSourceWithContent(
-    getState(),
-    location.sourceId
-  );
+  const source = getSourceWithContent(getState(), location.sourceId);
 
   let locations = null;
   if (location.line && source && !source.isWasm) {
     locations = await parser.findOutOfScopeLocations(
       source.id,
       ((location: any): parser.AstPosition)
     );
   }
 
   const linesOutOfScope = getOutOfScopeLines(locations);
   const sourceNumLines =
-    !content || !isFulfilled(content) ? 0 : getSourceLineCount(content.value);
+    !source.content || !isFulfilled(source.content)
+      ? 0
+      : getSourceLineCount(source.content.value);
 
   const sourceLines = range(1, sourceNumLines + 1);
 
   return !linesOutOfScope
     ? sourceLines
     : without(sourceLines, ...linesOutOfScope);
 }
 
--- a/devtools/client/debugger/src/actions/file-search.js
+++ b/devtools/client/debugger/src/actions/file-search.js
@@ -30,35 +30,35 @@ import {
   setActiveSearch,
 } from "./ui";
 import { isFulfilled } from "../utils/async-value";
 type Editor = Object;
 type Match = Object;
 
 export function doSearch(cx: Context, query: string, editor: Editor) {
   return ({ getState, dispatch }: ThunkArgs) => {
-    const selectedSourceWithContent = getSelectedSourceWithContent(getState());
-    if (!selectedSourceWithContent || !selectedSourceWithContent.content) {
+    const selectedSource = getSelectedSourceWithContent(getState());
+    if (!selectedSource || !selectedSource.content) {
       return;
     }
 
     dispatch(setFileSearchQuery(cx, query));
     dispatch(searchContents(cx, query, editor));
   };
 }
 
 export function doSearchForHighlight(
   query: string,
   editor: Editor,
   line: number,
   ch: number
 ) {
   return async ({ getState, dispatch }: ThunkArgs) => {
-    const selectedSourceWithContent = getSelectedSourceWithContent(getState());
-    if (!selectedSourceWithContent || !selectedSourceWithContent.content) {
+    const selectedSource = getSelectedSourceWithContent(getState());
+    if (!selectedSource || !selectedSource.content) {
       return;
     }
     dispatch(searchContentsForHighlight(query, editor, line, ch));
   };
 }
 
 export function setFileSearchQuery(cx: Context, query: string): Action {
   return {
@@ -100,29 +100,28 @@ export function updateSearchResults(
 export function searchContents(
   cx: Context,
   query: string,
   editor: Object,
   focusFirstResult?: boolean = true
 ) {
   return async ({ getState, dispatch }: ThunkArgs) => {
     const modifiers = getFileSearchModifiers(getState());
-    const selectedSourceWithContent = getSelectedSourceWithContent(getState());
+    const selectedSource = getSelectedSourceWithContent(getState());
 
     if (
       !editor ||
-      !selectedSourceWithContent ||
-      !selectedSourceWithContent.content ||
-      !isFulfilled(selectedSourceWithContent.content) ||
+      !selectedSource ||
+      !selectedSource.content ||
+      !isFulfilled(selectedSource.content) ||
       !modifiers
     ) {
       return;
     }
-    const selectedSource = selectedSourceWithContent.source;
-    const selectedContent = selectedSourceWithContent.content.value;
+    const selectedContent = selectedSource.content.value;
 
     const ctx = { ed: editor, cm: editor.codeMirror };
 
     if (!query) {
       clearSearch(ctx.cm, query);
       return;
     }
 
--- a/devtools/client/debugger/src/components/Editor/EditorMenu.js
+++ b/devtools/client/debugger/src/components/Editor/EditorMenu.js
@@ -25,53 +25,53 @@ import type SourceEditor from "../../uti
 type Props = {
   cx: ThreadContext,
   contextMenu: ?MouseEvent,
   editorActions: EditorItemActions,
   clearContextMenu: () => void,
   editor: SourceEditor,
   hasPrettySource: boolean,
   isPaused: boolean,
-  selectedSourceWithContent: SourceWithContent,
+  selectedSource: SourceWithContent,
 };
 
 class EditorMenu extends Component<Props> {
   props: Props;
 
   componentWillUpdate(nextProps: Props) {
     this.props.clearContextMenu();
     if (nextProps.contextMenu) {
       this.showMenu(nextProps);
     }
   }
 
   showMenu(props) {
     const {
       cx,
       editor,
-      selectedSourceWithContent,
+      selectedSource,
       editorActions,
       hasPrettySource,
       isPaused,
       contextMenu: event,
     } = props;
 
     const location = getSourceLocationFromMouseEvent(
       editor,
-      selectedSourceWithContent.source,
+      selectedSource,
       // Use a coercion, as contextMenu is optional
       (event: any)
     );
 
     showMenu(
       event,
       editorMenuItems({
         cx,
         editorActions,
-        selectedSourceWithContent,
+        selectedSource,
         hasPrettySource,
         location,
         isPaused,
         selectionText: editor.codeMirror.getSelection().trim(),
         isTextSelected: editor.codeMirror.somethingSelected(),
       })
     );
   }
@@ -79,20 +79,17 @@ class EditorMenu extends Component<Props
   render() {
     return null;
   }
 }
 
 const mapStateToProps = (state, props) => ({
   cx: getThreadContext(state),
   isPaused: getIsPaused(state, getCurrentThread(state)),
-  hasPrettySource: !!getPrettySource(
-    state,
-    props.selectedSourceWithContent.source.id
-  ),
+  hasPrettySource: !!getPrettySource(state, props.selectedSource.id),
 });
 
 const mapDispatchToProps = dispatch => ({
   editorActions: editorItemActions(dispatch),
 });
 
 export default connect(
   mapStateToProps,
--- a/devtools/client/debugger/src/components/Editor/Footer.js
+++ b/devtools/client/debugger/src/components/Editor/Footer.js
@@ -33,17 +33,17 @@ import "./Footer.css";
 
 type CursorPosition = {
   line: number,
   column: number,
 };
 
 type Props = {
   cx: Context,
-  selectedSourceWithContent: ?SourceWithContent,
+  selectedSource: ?SourceWithContent,
   mappedSource: Source,
   endPanelCollapsed: boolean,
   horizontal: boolean,
   togglePrettyPrint: typeof actions.togglePrettyPrint,
   toggleBlackBox: typeof actions.toggleBlackBox,
   jumpToMappedLocation: typeof actions.jumpToMappedLocation,
   togglePaneCollapse: typeof actions.togglePaneCollapse,
 };
@@ -79,93 +79,86 @@ class SourceFooter extends PureComponent
     if (toggle === true) {
       eventDoc.CodeMirror.on("cursorActivity", this.onCursorChange);
     } else {
       eventDoc.CodeMirror.off("cursorActivity", this.onCursorChange);
     }
   }
 
   prettyPrintButton() {
-    const { cx, selectedSourceWithContent, togglePrettyPrint } = this.props;
+    const { cx, selectedSource, togglePrettyPrint } = this.props;
 
-    if (!selectedSourceWithContent) {
+    if (!selectedSource) {
       return;
     }
 
-    if (
-      !selectedSourceWithContent.content &&
-      selectedSourceWithContent.source.isPrettyPrinted
-    ) {
+    if (!selectedSource.content && selectedSource.isPrettyPrinted) {
       return (
         <div className="loader" key="pretty-loader">
           <AccessibleImage className="loader" />
         </div>
       );
     }
 
     const sourceContent =
-      selectedSourceWithContent.content &&
-      isFulfilled(selectedSourceWithContent.content)
-        ? selectedSourceWithContent.content.value
+      selectedSource.content && isFulfilled(selectedSource.content)
+        ? selectedSource.content.value
         : null;
     if (
       !shouldShowPrettyPrint(
-        selectedSourceWithContent.source,
+        selectedSource,
         sourceContent || { type: "text", value: "", contentType: undefined }
       )
     ) {
       return;
     }
 
     const tooltip = L10N.getStr("sourceTabs.prettyPrint");
-    const sourceLoaded = !!selectedSourceWithContent.content;
+    const sourceLoaded = !!selectedSource.content;
 
     const type = "prettyPrint";
     return (
       <button
-        onClick={() =>
-          togglePrettyPrint(cx, selectedSourceWithContent.source.id)
-        }
+        onClick={() => togglePrettyPrint(cx, selectedSource.id)}
         className={classnames("action", type, {
           active: sourceLoaded,
-          pretty: isPretty(selectedSourceWithContent.source),
+          pretty: isPretty(selectedSource),
         })}
         key={type}
         title={tooltip}
         aria-label={tooltip}
       >
         <AccessibleImage className={type} />
       </button>
     );
   }
 
   blackBoxButton() {
-    const { cx, selectedSourceWithContent, toggleBlackBox } = this.props;
-    const sourceLoaded =
-      selectedSourceWithContent && selectedSourceWithContent.content;
+    const { cx, selectedSource, toggleBlackBox } = this.props;
+    const sourceLoaded = selectedSource && selectedSource.content;
 
-    if (!selectedSourceWithContent) {
+    if (!selectedSource) {
       return;
     }
 
-    if (!shouldBlackbox(selectedSourceWithContent.source)) {
+    if (!shouldBlackbox(selectedSource)) {
       return;
     }
 
-    const blackboxed = selectedSourceWithContent.source.isBlackBoxed;
+    const blackboxed = selectedSource.isBlackBoxed;
 
     const tooltip = blackboxed
       ? L10N.getStr("sourceFooter.unblackbox")
       : L10N.getStr("sourceFooter.blackbox");
 
     const type = "black-box";
 
     return (
       <button
-        onClick={() => toggleBlackBox(cx, selectedSourceWithContent.source)}
+        onClick={() => toggleBlackBox(cx, selectedSource)}
         className={classnames("action", type, {
           active: sourceLoaded,
           blackboxed: blackboxed,
         })}
         key={type}
         title={tooltip}
         aria-label={tooltip}
       >
@@ -198,35 +191,31 @@ class SourceFooter extends PureComponent
     return commands.length ? <div className="commands">{commands}</div> : null;
   }
 
   renderSourceSummary() {
     const {
       cx,
       mappedSource,
       jumpToMappedLocation,
-      selectedSourceWithContent,
+      selectedSource,
     } = this.props;
 
-    if (
-      !mappedSource ||
-      !selectedSourceWithContent ||
-      !isOriginal(selectedSourceWithContent.source)
-    ) {
+    if (!mappedSource || !selectedSource || !isOriginal(selectedSource)) {
       return null;
     }
 
     const filename = getFilename(mappedSource);
     const tooltip = L10N.getFormatStr(
       "sourceFooter.mappedSourceTooltip",
       filename
     );
     const title = L10N.getFormatStr("sourceFooter.mappedSource", filename);
     const mappedSourceLocation = {
-      sourceId: selectedSourceWithContent.source.id,
+      sourceId: selectedSource.id,
       line: 1,
       column: 1,
     };
     return (
       <button
         className="mapped-source"
         onClick={() => jumpToMappedLocation(cx, mappedSourceLocation)}
         title={tooltip}
@@ -237,17 +226,17 @@ class SourceFooter extends PureComponent
   }
 
   onCursorChange = event => {
     const { line, ch } = event.doc.getCursor();
     this.setState({ cursorPosition: { line, column: ch } });
   };
 
   renderCursorPosition() {
-    if (!this.props.selectedSourceWithContent) {
+    if (!this.props.selectedSource) {
       return null;
     }
 
     const { line, column } = this.state.cursorPosition;
 
     const text = L10N.getFormatStr(
       "sourceFooter.currentCursorPosition",
       line + 1,
@@ -275,28 +264,25 @@ class SourceFooter extends PureComponent
           {this.renderToggleButton()}
         </div>
       </div>
     );
   }
 }
 
 const mapStateToProps = state => {
-  const selectedSourceWithContent = getSelectedSourceWithContent(state);
+  const selectedSource = getSelectedSourceWithContent(state);
 
   return {
     cx: getContext(state),
-    selectedSourceWithContent,
-    mappedSource: getGeneratedSource(
-      state,
-      selectedSourceWithContent && selectedSourceWithContent.source
-    ),
+    selectedSource,
+    mappedSource: getGeneratedSource(state, selectedSource),
     prettySource: getPrettySource(
       state,
-      selectedSourceWithContent ? selectedSourceWithContent.source.id : null
+      selectedSource ? selectedSource.id : null
     ),
     endPanelCollapsed: getPaneCollapse(state, "end"),
   };
 };
 
 export default connect(
   mapStateToProps,
   {
--- a/devtools/client/debugger/src/components/Editor/HighlightLine.js
+++ b/devtools/client/debugger/src/components/Editor/HighlightLine.js
@@ -23,115 +23,103 @@ import type {
   SourceDocuments,
 } from "../../types";
 import type { Command } from "../../reducers/types";
 
 type Props = {
   pauseCommand: Command,
   selectedFrame: Frame,
   selectedLocation: SourceLocation,
-  selectedSourceWithContent: ?SourceWithContent,
+  selectedSource: ?SourceWithContent,
 };
 
 function isDebugLine(selectedFrame: Frame, selectedLocation: SourceLocation) {
   if (!selectedFrame) {
     return;
   }
 
   return (
     selectedFrame.location.sourceId == selectedLocation.sourceId &&
     selectedFrame.location.line == selectedLocation.line
   );
 }
 
-function isDocumentReady(
-  selectedSourceWithContent: ?SourceWithContent,
-  selectedLocation
-) {
+function isDocumentReady(selectedSource: ?SourceWithContent, selectedLocation) {
   return (
     selectedLocation &&
-    selectedSourceWithContent &&
-    selectedSourceWithContent.content &&
+    selectedSource &&
+    selectedSource.content &&
     hasDocument(selectedLocation.sourceId)
   );
 }
 
 export class HighlightLine extends Component<Props> {
   isStepping: boolean = false;
   previousEditorLine: ?number = null;
 
   shouldComponentUpdate(nextProps: Props) {
-    const { selectedLocation, selectedSourceWithContent } = nextProps;
-    return this.shouldSetHighlightLine(
-      selectedLocation,
-      selectedSourceWithContent
-    );
+    const { selectedLocation, selectedSource } = nextProps;
+    return this.shouldSetHighlightLine(selectedLocation, selectedSource);
   }
 
   componentDidUpdate(prevProps: Props) {
     this.completeHighlightLine(prevProps);
   }
 
   componentDidMount() {
     this.completeHighlightLine(null);
   }
 
   shouldSetHighlightLine(
     selectedLocation: SourceLocation,
-    selectedSourceWithContent: ?SourceWithContent
+    selectedSource: ?SourceWithContent
   ) {
     const { sourceId, line } = selectedLocation;
     const editorLine = toEditorLine(sourceId, line);
 
-    if (!isDocumentReady(selectedSourceWithContent, selectedLocation)) {
+    if (!isDocumentReady(selectedSource, selectedLocation)) {
       return false;
     }
 
     if (this.isStepping && editorLine === this.previousEditorLine) {
       return false;
     }
 
     return true;
   }
 
   completeHighlightLine(prevProps: Props | null) {
     const {
       pauseCommand,
       selectedLocation,
       selectedFrame,
-      selectedSourceWithContent,
+      selectedSource,
     } = this.props;
     if (pauseCommand) {
       this.isStepping = true;
     }
 
     startOperation();
     if (prevProps) {
       this.clearHighlightLine(
         prevProps.selectedLocation,
-        prevProps.selectedSourceWithContent
+        prevProps.selectedSource
       );
     }
-    this.setHighlightLine(
-      selectedLocation,
-      selectedFrame,
-      selectedSourceWithContent
-    );
+    this.setHighlightLine(selectedLocation, selectedFrame, selectedSource);
     endOperation();
   }
 
   setHighlightLine(
     selectedLocation: SourceLocation,
     selectedFrame: Frame,
-    selectedSourceWithContent: ?SourceWithContent
+    selectedSource: ?SourceWithContent
   ) {
     const { sourceId, line } = selectedLocation;
-    if (
-      !this.shouldSetHighlightLine(selectedLocation, selectedSourceWithContent)
-    ) {
+    if (!this.shouldSetHighlightLine(selectedLocation, selectedSource)) {
       return;
     }
 
     this.isStepping = false;
     const editorLine = toEditorLine(sourceId, line);
     this.previousEditorLine = editorLine;
 
     if (!line || isDebugLine(selectedFrame, selectedLocation)) {
@@ -161,19 +149,19 @@ export class HighlightLine extends Compo
     setTimeout(
       () => doc && doc.removeLineClass(editorLine, "line", "highlight-line"),
       duration
     );
   }
 
   clearHighlightLine(
     selectedLocation: SourceLocation,
-    selectedSourceWithContent: ?SourceWithContent
+    selectedSource: ?SourceWithContent
   ) {
-    if (!isDocumentReady(selectedSourceWithContent, selectedLocation)) {
+    if (!isDocumentReady(selectedSource, selectedLocation)) {
       return;
     }
 
     const { line, sourceId } = selectedLocation;
     const editorLine = toEditorLine(sourceId, line);
     const doc = getDocument(sourceId);
     doc.removeLineClass(editorLine, "line", "highlight-line");
   }
@@ -182,10 +170,10 @@ export class HighlightLine extends Compo
     return null;
   }
 }
 
 export default connect(state => ({
   pauseCommand: getPauseCommand(state, getCurrentThread(state)),
   selectedFrame: getVisibleSelectedFrame(state),
   selectedLocation: getSelectedLocation(state),
-  selectedSourceWithContent: getSelectedSourceWithContent(state),
+  selectedSource: getSelectedSourceWithContent(state),
 }))(HighlightLine);
--- a/devtools/client/debugger/src/components/Editor/index.js
+++ b/devtools/client/debugger/src/components/Editor/index.js
@@ -90,17 +90,17 @@ import type {
 
 const cssVars = {
   searchbarHeight: "var(--editor-searchbar-height)",
 };
 
 export type Props = {
   cx: ThreadContext,
   selectedLocation: ?SourceLocation,
-  selectedSourceWithContent: ?SourceWithContent,
+  selectedSource: ?SourceWithContent,
   searchOn: boolean,
   startPanelSize: number,
   endPanelSize: number,
   conditionalPanelLocation: SourceLocation,
   symbols: SymbolDeclarations,
   isPaused: boolean,
   skipPausing: boolean,
 
@@ -134,30 +134,27 @@ class Editor extends PureComponent<Props
       editor: (null: any),
       contextMenu: null,
     };
   }
 
   componentWillReceiveProps(nextProps: Props) {
     let editor = this.state.editor;
 
-    if (!this.state.editor && nextProps.selectedSourceWithContent) {
+    if (!this.state.editor && nextProps.selectedSource) {
       editor = this.setupEditor();
     }
 
     startOperation();
     this.setText(nextProps, editor);
     this.setSize(nextProps, editor);
     this.scrollToLocation(nextProps, editor);
     endOperation();
 
-    if (
-      this.props.selectedSourceWithContent !=
-      nextProps.selectedSourceWithContent
-    ) {
+    if (this.props.selectedSource != nextProps.selectedSource) {
       this.props.updateViewport();
       resizeBreakpointGutter(editor.codeMirror);
       resizeToggleButton(editor.codeMirror);
     }
   }
 
   setupEditor() {
     const editor = getEditor();
@@ -225,21 +222,21 @@ class Editor extends PureComponent<Props
       L10N.getStr("toggleCondPanel.logPoint.key"),
       this.onToggleConditionalPanel
     );
     shortcuts.on(L10N.getStr("sourceTabs.closeTab.key"), this.onClosePress);
     shortcuts.on("Esc", this.onEscape);
   }
 
   onClosePress = (key, e: KeyboardEvent) => {
-    const { cx, selectedSourceWithContent } = this.props;
-    if (selectedSourceWithContent) {
+    const { cx, selectedSource } = this.props;
+    if (selectedSource) {
       e.preventDefault();
       e.stopPropagation();
-      this.props.closeTab(cx, selectedSourceWithContent.source);
+      this.props.closeTab(cx, selectedSource);
     }
   };
 
   componentWillUnmount() {
     if (this.state.editor) {
       this.state.editor.destroy();
       this.state.editor.codeMirror.off("scroll", this.onEditorScroll);
       this.setState({ editor: (null: any) });
@@ -249,23 +246,23 @@ class Editor extends PureComponent<Props
     shortcuts.off(L10N.getStr("sourceTabs.closeTab.key"));
     shortcuts.off(L10N.getStr("toggleBreakpoint.key"));
     shortcuts.off(L10N.getStr("toggleCondPanel.breakpoint.key"));
     shortcuts.off(L10N.getStr("toggleCondPanel.logPoint.key"));
   }
 
   getCurrentLine() {
     const { codeMirror } = this.state.editor;
-    const { selectedSourceWithContent } = this.props;
-    if (!selectedSourceWithContent) {
+    const { selectedSource } = this.props;
+    if (!selectedSource) {
       return;
     }
 
     const line = getCursorLine(codeMirror);
-    return toSourceLine(selectedSourceWithContent.source.id, line);
+    return toSourceLine(selectedSource.id, line);
   }
 
   onToggleBreakpoint = (key, e: KeyboardEvent) => {
     e.preventDefault();
     e.stopPropagation();
 
     const line = this.getCurrentLine();
     if (typeof line !== "number") {
@@ -326,35 +323,35 @@ class Editor extends PureComponent<Props
   };
 
   openMenu(event: MouseEvent) {
     event.stopPropagation();
     event.preventDefault();
 
     const {
       cx,
-      selectedSourceWithContent,
+      selectedSource,
       breakpointActions,
       editorActions,
       isPaused,
       conditionalPanelLocation,
       closeConditionalPanel,
     } = this.props;
     const { editor } = this.state;
-    if (!selectedSourceWithContent || !editor) {
+    if (!selectedSource || !editor) {
       return;
     }
 
     // only allow one conditionalPanel location.
     if (conditionalPanelLocation) {
       closeConditionalPanel();
     }
 
     const target: Element = (event.target: any);
-    const { id: sourceId } = selectedSourceWithContent.source;
+    const { id: sourceId } = selectedSource;
     const line = lineAtHeight(editor, sourceId, event);
 
     if (typeof line != "number") {
       return;
     }
 
     const location = { line, column: undefined, sourceId };
 
@@ -380,136 +377,126 @@ class Editor extends PureComponent<Props
   onGutterClick = (
     cm: Object,
     line: number,
     gutter: string,
     ev: MouseEvent
   ) => {
     const {
       cx,
-      selectedSourceWithContent,
+      selectedSource,
       conditionalPanelLocation,
       closeConditionalPanel,
       addBreakpointAtLine,
       continueToHere,
       toggleBlackBox,
     } = this.props;
 
     // ignore right clicks in the gutter
-    if (
-      (ev.ctrlKey && ev.button === 0) ||
-      ev.button === 2 ||
-      !selectedSourceWithContent
-    ) {
+    if ((ev.ctrlKey && ev.button === 0) || ev.button === 2 || !selectedSource) {
       return;
     }
 
     // if user clicks gutter to set breakpoint on blackboxed source, un-blackbox the source.
-    if (
-      selectedSourceWithContent &&
-      selectedSourceWithContent.source.isBlackBoxed
-    ) {
-      toggleBlackBox(cx, selectedSourceWithContent.source);
+    if (selectedSource && selectedSource.isBlackBoxed) {
+      toggleBlackBox(cx, selectedSource);
     }
 
     if (conditionalPanelLocation) {
       return closeConditionalPanel();
     }
 
     if (gutter === "CodeMirror-foldgutter") {
       return;
     }
 
-    const sourceLine = toSourceLine(selectedSourceWithContent.source.id, line);
+    const sourceLine = toSourceLine(selectedSource.id, line);
     if (typeof sourceLine !== "number") {
       return;
     }
 
     if (ev.metaKey) {
       return continueToHere(cx, sourceLine);
     }
 
     return addBreakpointAtLine(cx, sourceLine, ev.altKey, ev.shiftKey);
   };
 
   onGutterContextMenu = (event: MouseEvent) => {
     return this.openMenu(event);
   };
 
   onClick(e: MouseEvent) {
-    const { cx, selectedSourceWithContent, jumpToMappedLocation } = this.props;
+    const { cx, selectedSource, jumpToMappedLocation } = this.props;
 
-    if (selectedSourceWithContent && e.metaKey && e.altKey) {
+    if (selectedSource && e.metaKey && e.altKey) {
       const sourceLocation = getSourceLocationFromMouseEvent(
         this.state.editor,
-        selectedSourceWithContent.source,
+        selectedSource,
         e
       );
       jumpToMappedLocation(cx, sourceLocation);
     }
   }
 
   toggleConditionalPanel = (line, log: boolean = false) => {
     const {
       conditionalPanelLocation,
       closeConditionalPanel,
       openConditionalPanel,
-      selectedSourceWithContent,
+      selectedSource,
     } = this.props;
 
     if (conditionalPanelLocation) {
       return closeConditionalPanel();
     }
 
-    if (!selectedSourceWithContent) {
+    if (!selectedSource) {
       return;
     }
 
     return openConditionalPanel(
       {
         line: line,
-        sourceId: selectedSourceWithContent.source.id,
-        sourceUrl: selectedSourceWithContent.source.url,
+        sourceId: selectedSource.id,
+        sourceUrl: selectedSource.url,
       },
       log
     );
   };
 
   shouldScrollToLocation(nextProps, editor) {
-    const { selectedLocation, selectedSourceWithContent } = this.props;
+    const { selectedLocation, selectedSource } = this.props;
     if (
       !editor ||
-      !nextProps.selectedSourceWithContent ||
+      !nextProps.selectedSource ||
       !nextProps.selectedLocation ||
       !nextProps.selectedLocation.line ||
-      !nextProps.selectedSourceWithContent.content
+      !nextProps.selectedSource.content
     ) {
       return false;
     }
 
     const isFirstLoad =
-      (!selectedSourceWithContent || !selectedSourceWithContent.content) &&
-      nextProps.selectedSourceWithContent.content;
+      (!selectedSource || !selectedSource.content) &&
+      nextProps.selectedSource.content;
     const locationChanged = selectedLocation !== nextProps.selectedLocation;
     const symbolsChanged = nextProps.symbols != this.props.symbols;
 
     return isFirstLoad || locationChanged || symbolsChanged;
   }
 
   scrollToLocation(nextProps, editor) {
-    const { selectedLocation, selectedSourceWithContent } = nextProps;
+    const { selectedLocation, selectedSource } = nextProps;
 
     if (selectedLocation && this.shouldScrollToLocation(nextProps, editor)) {
       let { line, column } = toEditorPosition(selectedLocation);
 
-      if (
-        selectedSourceWithContent &&
-        hasDocument(selectedSourceWithContent.source.id)
-      ) {
-        const doc = getDocument(selectedSourceWithContent.source.id);
+      if (selectedSource && hasDocument(selectedSource.id)) {
+        const doc = getDocument(selectedSource.id);
         const lineText: ?string = doc.getLine(line);
         column = Math.max(column, getIndentation(lineText));
       }
 
       scrollToColumn(editor.codeMirror, line, column);
     }
   }
 
@@ -522,44 +509,44 @@ class Editor extends PureComponent<Props
       nextProps.startPanelSize !== this.props.startPanelSize ||
       nextProps.endPanelSize !== this.props.endPanelSize
     ) {
       editor.codeMirror.setSize();
     }
   }
 
   setText(props, editor) {
-    const { selectedSourceWithContent, symbols } = props;
+    const { selectedSource, symbols } = props;
 
     if (!editor) {
       return;
     }
 
     // check if we previously had a selected source
-    if (!selectedSourceWithContent) {
+    if (!selectedSource) {
       return this.clearEditor();
     }
 
-    if (!selectedSourceWithContent.content) {
+    if (!selectedSource.content) {
       return showLoading(editor);
     }
 
-    if (selectedSourceWithContent.content.state === "rejected") {
-      let { value } = selectedSourceWithContent.content;
+    if (selectedSource.content.state === "rejected") {
+      let { value } = selectedSource.content;
       if (typeof value !== "string") {
         value = "Unexpected source error";
       }
 
       return this.showErrorMessage(value);
     }
 
     return showSourceText(
       editor,
-      selectedSourceWithContent.source,
-      selectedSourceWithContent.content.value,
+      selectedSource,
+      selectedSource.content.value,
       symbols
     );
   }
 
   clearEditor() {
     const { editor } = this.state;
     if (!editor) {
       return;
@@ -589,78 +576,69 @@ class Editor extends PureComponent<Props
     return {
       height: "100%",
     };
   }
 
   renderItems() {
     const {
       cx,
-      selectedSourceWithContent,
+      selectedSource,
       conditionalPanelLocation,
       isPaused,
     } = this.props;
     const { editor, contextMenu } = this.state;
 
-    if (
-      !selectedSourceWithContent ||
-      !editor ||
-      !getDocument(selectedSourceWithContent.source.id)
-    ) {
+    if (!selectedSource || !editor || !getDocument(selectedSource.id)) {
       return null;
     }
 
     return (
       <div>
         <DebugLine editor={editor} />
         <HighlightLine />
         <EmptyLines editor={editor} />
         <Breakpoints editor={editor} cx={cx} />
         <Preview editor={editor} editorRef={this.$editorWrapper} />
         <HighlightLines editor={editor} />
         {
           <EditorMenu
             editor={editor}
             contextMenu={contextMenu}
             clearContextMenu={this.clearContextMenu}
-            selectedSourceWithContent={selectedSourceWithContent}
+            selectedSource={selectedSource}
           />
         }
         {conditionalPanelLocation ? <ConditionalPanel editor={editor} /> : null}
         {features.columnBreakpoints ? (
           <ColumnBreakpoints editor={editor} />
         ) : null}
         {isPaused && features.inlinePreview ? (
-          <InlinePreviews
-            editor={editor}
-            selectedSource={selectedSourceWithContent.source}
-          />
+          <InlinePreviews editor={editor} selectedSource={selectedSource} />
         ) : null}
       </div>
     );
   }
 
   renderSearchBar() {
     const { editor } = this.state;
 
-    if (!this.props.selectedSourceWithContent) {
+    if (!this.props.selectedSource) {
       return null;
     }
 
     return <SearchBar editor={editor} />;
   }
 
   render() {
-    const { selectedSourceWithContent, skipPausing } = this.props;
+    const { selectedSource, skipPausing } = this.props;
     return (
       <div
         className={classnames("editor-wrapper", {
-          blackboxed:
-            selectedSourceWithContent &&
-            selectedSourceWithContent.source.isBlackBoxed,
+          blackboxed: selectedSource && selectedSource.isBlackBoxed,
           "skip-pausing": skipPausing,
         })}
         ref={c => (this.$editorWrapper = c)}
       >
         <div
           className="editor-mount devtools-monospace"
           style={this.getInlineEditorStyles()}
         />
@@ -671,28 +649,25 @@ class Editor extends PureComponent<Props
   }
 }
 
 Editor.contextTypes = {
   shortcuts: PropTypes.object,
 };
 
 const mapStateToProps = state => {
-  const selectedSourceWithContent = getSelectedSourceWithContent(state);
+  const selectedSource = getSelectedSourceWithContent(state);
 
   return {
     cx: getThreadContext(state),
     selectedLocation: getSelectedLocation(state),
-    selectedSourceWithContent,
+    selectedSource,
     searchOn: getActiveSearch(state) === "file",
     conditionalPanelLocation: getConditionalPanelLocation(state),
-    symbols: getSymbols(
-      state,
-      selectedSourceWithContent ? selectedSourceWithContent.source : null
-    ),
+    symbols: getSymbols(state, selectedSource),
     isPaused: getIsPaused(state, getCurrentThread(state)),
     skipPausing: getSkipPausing(state),
   };
 };
 
 const mapDispatchToProps = dispatch => ({
   ...bindActionCreators(
     {
--- a/devtools/client/debugger/src/components/Editor/menus/editor.js
+++ b/devtools/client/debugger/src/components/Editor/menus/editor.js
@@ -163,56 +163,58 @@ const downloadFileItem = (
   label: L10N.getStr("downloadFile.label"),
   accesskey: L10N.getStr("downloadFile.accesskey"),
   click: () => downloadFile(selectedContent, getFilename(selectedSource)),
 });
 
 export function editorMenuItems({
   cx,
   editorActions,
-  selectedSourceWithContent,
+  selectedSource,
   location,
   selectionText,
   hasPrettySource,
   isTextSelected,
   isPaused,
 }: {
   cx: ThreadContext,
   editorActions: EditorItemActions,
-  selectedSourceWithContent: SourceWithContent,
+  selectedSource: SourceWithContent,
   location: SourceLocation,
   selectionText: string,
   hasPrettySource: boolean,
   isTextSelected: boolean,
   isPaused: boolean,
 }) {
   const items = [];
-  const { source: selectedSource, content } = selectedSourceWithContent;
+
+  const content =
+    selectedSource.content && isFulfilled(selectedSource.content)
+      ? selectedSource.content.value
+      : null;
 
   items.push(
     jumpToMappedLocationItem(
       cx,
       selectedSource,
       location,
       hasPrettySource,
       editorActions
     ),
     continueToHereItem(cx, location, isPaused, editorActions),
     { type: "separator" },
-    ...(content && isFulfilled(content)
-      ? [copyToClipboardItem(content.value, editorActions)]
-      : []),
+    ...(content ? [copyToClipboardItem(content, editorActions)] : []),
     ...(!selectedSource.isWasm
       ? [
           copySourceItem(selectedSource, selectionText, editorActions),
           copySourceUri2Item(selectedSource, editorActions),
         ]
       : []),
-    ...(content && isFulfilled(content)
-      ? [downloadFileItem(selectedSource, content.value, editorActions)]
+    ...(content
+      ? [downloadFileItem(selectedSource, content, editorActions)]
       : []),
     { type: "separator" },
     showSourceMenuItem(cx, selectedSource, editorActions),
     blackBoxMenuItem(cx, selectedSource, editorActions)
   );
 
   if (isTextSelected) {
     items.push(
--- a/devtools/client/debugger/src/components/Editor/tests/DebugLine.spec.js
+++ b/devtools/client/debugger/src/components/Editor/tests/DebugLine.spec.js
@@ -29,17 +29,17 @@ function createMockDocument(clear) {
 function generateDefaults(editor, overrides) {
   return {
     editor,
     pauseInfo: {
       why: { type: "breakpoint" },
     },
     frame: null,
     source: ({
-      source: createSourceObject("foo"),
+      ...createSourceObject("foo"),
       content: null,
     }: SourceWithContent),
     ...overrides,
   };
 }
 
 function createFrame(line) {
   return {
@@ -52,31 +52,31 @@ function createFrame(line) {
 }
 
 function render(overrides = {}) {
   const clear = jest.fn();
   const editor = { codeMirror: {} };
   const props = generateDefaults(editor, overrides);
 
   const doc = createMockDocument(clear);
-  setDocument(props.source.source.id, doc);
+  setDocument(props.source.id, doc);
 
   // $FlowIgnore
   const component = shallow(<DebugLine.WrappedComponent {...props} />, {
     lifecycleExperimental: true,
   });
   return { component, props, clear, editor, doc };
 }
 
 describe("DebugLine Component", () => {
   describe("pausing at the first location", () => {
     it("should show a new debug line", async () => {
       const { component, props, doc } = render({
         source: {
-          source: createSourceObject("foo"),
+          ...createSourceObject("foo"),
           content: asyncValue.fulfilled({
             type: "text",
             value: "",
             contentType: undefined,
           }),
         },
       });
       const line = 2;
@@ -89,17 +89,17 @@ describe("DebugLine Component", () => {
         [toEditorLine("foo", line), "line", "new-debug-line"],
       ]);
     });
 
     describe("pausing at a new location", () => {
       it("should replace the first debug line", async () => {
         const { props, component, clear, doc } = render({
           source: {
-            source: createSourceObject("foo"),
+            ...createSourceObject("foo"),
             content: asyncValue.fulfilled({
               type: "text",
               value: "",
               contentType: undefined,
             }),
           },
         });
 
--- a/devtools/client/debugger/src/components/Editor/tests/Editor.spec.js
+++ b/devtools/client/debugger/src/components/Editor/tests/Editor.spec.js
@@ -2,17 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 
 import React from "react";
 import { shallow } from "enzyme";
 import Editor from "../index";
-import type { Source, SourceWithContent } from "../../../types";
+import type { Source, SourceWithContent, SourceBase } from "../../../types";
 import { getDocument } from "../../../utils/editor/source-documents";
 import * as asyncValue from "../../../utils/async-value";
 
 function generateDefaults(overrides) {
   return {
     toggleBreakpoint: jest.fn(),
     updateViewport: jest.fn(),
     toggleDisabledBreakpoint: jest.fn(),
@@ -69,17 +69,17 @@ function createMockSourceWithContent(
   const {
     loadedState = "loaded",
     text = "the text",
     contentType = undefined,
     error = undefined,
     ...otherOverrides
   } = overrides;
 
-  const source: Source = ({
+  const source: SourceBase = ({
     id: "foo",
     url: "foo",
     ...otherOverrides,
   }: any);
   let content = null;
   if (loadedState === "loaded") {
     if (typeof text !== "string") {
       throw new Error("Cannot create a non-text source");
@@ -90,17 +90,17 @@ function createMockSourceWithContent(
       : asyncValue.fulfilled({
           type: "text",
           value: text,
           contentType: contentType || undefined,
         });
   }
 
   return {
-    source,
+    ...source,
     content,
   };
 }
 
 function render(overrides = {}) {
   const props = generateDefaults(overrides);
   const mockEditor = createMockEditor();
 
@@ -123,17 +123,17 @@ describe("Editor", () => {
     });
   });
 
   describe("When loading initial source", () => {
     it("should show a loading message", async () => {
       const { component, mockEditor } = render();
       await component.setState({ editor: mockEditor });
       component.setProps({
-        selectedSourceWithContent: {
+        selectedSource: {
           source: { loadedState: "loading" },
           content: null,
         },
       });
 
       expect(mockEditor.replaceDocument.mock.calls[0][0].getValue()).toBe(
         "Loading…"
       );
@@ -143,17 +143,17 @@ describe("Editor", () => {
 
   describe("When loaded", () => {
     it("should show text", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
       await component.setProps({
         ...props,
-        selectedSourceWithContent: createMockSourceWithContent({
+        selectedSource: createMockSourceWithContent({
           loadedState: "loaded",
         }),
         selectedLocation: { sourceId: "foo", line: 3, column: 1 },
       });
 
       expect(mockEditor.setText.mock.calls).toEqual([["the text"]]);
       expect(mockEditor.codeMirror.scrollTo.mock.calls).toEqual([[1, 2]]);
     });
@@ -161,17 +161,17 @@ describe("Editor", () => {
 
   describe("When error", () => {
     it("should show error text", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
       await component.setProps({
         ...props,
-        selectedSourceWithContent: createMockSourceWithContent({
+        selectedSource: createMockSourceWithContent({
           loadedState: "loaded",
           text: undefined,
           error: "error text",
         }),
         selectedLocation: { sourceId: "bad-foo", line: 3, column: 1 },
       });
 
       expect(mockEditor.setText.mock.calls).toEqual([
@@ -180,17 +180,17 @@ describe("Editor", () => {
     });
 
     it("should show wasm error", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
       await component.setProps({
         ...props,
-        selectedSourceWithContent: createMockSourceWithContent({
+        selectedSource: createMockSourceWithContent({
           loadedState: "loaded",
           isWasm: true,
           text: undefined,
           error: "blah WebAssembly binary source is not available blah",
         }),
         selectedLocation: { sourceId: "bad-foo", line: 3, column: 1 },
       });
 
@@ -202,26 +202,26 @@ describe("Editor", () => {
 
   describe("When navigating to a loading source", () => {
     it("should show loading message and not scroll", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
       await component.setProps({
         ...props,
-        selectedSourceWithContent: createMockSourceWithContent({
+        selectedSource: createMockSourceWithContent({
           loadedState: "loaded",
         }),
         selectedLocation: { sourceId: "foo", line: 3, column: 1 },
       });
 
       // navigate to a new source that is still loading
       await component.setProps({
         ...props,
-        selectedSourceWithContent: createMockSourceWithContent({
+        selectedSource: createMockSourceWithContent({
           id: "bar",
           loadedState: "loading",
         }),
         selectedLocation: { sourceId: "bar", line: 1, column: 1 },
       });
 
       expect(mockEditor.replaceDocument.mock.calls[1][0].getValue()).toBe(
         "Loading…"
@@ -232,66 +232,64 @@ describe("Editor", () => {
       expect(mockEditor.codeMirror.scrollTo.mock.calls).toEqual([[1, 2]]);
     });
 
     it("should set the mode when symbols load", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
 
-      const selectedSourceWithContent = createMockSourceWithContent({
+      const selectedSource = createMockSourceWithContent({
         loadedState: "loaded",
         contentType: "javascript",
       });
 
-      await component.setProps({ ...props, selectedSourceWithContent });
+      await component.setProps({ ...props, selectedSource });
 
       const symbols = { hasJsx: true };
       await component.setProps({
         ...props,
-        selectedSourceWithContent,
+        selectedSource,
         symbols,
       });
 
       expect(mockEditor.setMode.mock.calls).toEqual([
         [{ name: "javascript" }],
         [{ name: "jsx" }],
       ]);
     });
 
     it("should not re-set the mode when the location changes", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
 
-      const selectedSourceWithContent = createMockSourceWithContent({
+      const selectedSource = createMockSourceWithContent({
         loadedState: "loaded",
         contentType: "javascript",
       });
 
-      await component.setProps({ ...props, selectedSourceWithContent });
+      await component.setProps({ ...props, selectedSource });
 
       // symbols are parsed
       const symbols = { hasJsx: true };
       await component.setProps({
         ...props,
-        selectedSourceWithContent,
+        selectedSource,
         symbols,
       });
 
       // selectedLocation changes e.g. pausing/stepping
-      mockEditor.codeMirror.doc = getDocument(
-        selectedSourceWithContent.source.id
-      );
+      mockEditor.codeMirror.doc = getDocument(selectedSource.id);
       mockEditor.codeMirror.getOption = () => ({ name: "jsx" });
       const selectedLocation = { sourceId: "foo", line: 4, column: 1 };
 
       await component.setProps({
         ...props,
-        selectedSourceWithContent,
+        selectedSource,
         symbols,
         selectedLocation,
       });
 
       expect(mockEditor.setMode.mock.calls).toEqual([
         [{ name: "javascript" }],
         [{ name: "jsx" }],
       ]);
@@ -300,26 +298,26 @@ describe("Editor", () => {
 
   describe("When navigating to a loaded source", () => {
     it("should show text and then scroll", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
       await component.setProps({
         ...props,
-        selectedSourceWithContent: createMockSourceWithContent({
+        selectedSource: createMockSourceWithContent({
           loadedState: "loading",
         }),
         selectedLocation: { sourceId: "foo", line: 1, column: 1 },
       });
 
       // navigate to a new source that is still loading
       await component.setProps({
         ...props,
-        selectedSourceWithContent: createMockSourceWithContent({
+        selectedSource: createMockSourceWithContent({
           loadedState: "loaded",
         }),
         selectedLocation: { sourceId: "foo", line: 1, column: 1 },
       });
 
       expect(mockEditor.replaceDocument.mock.calls[0][0].getValue()).toBe(
         "Loading…"
       );
--- a/devtools/client/debugger/src/components/Editor/tests/Footer.spec.js
+++ b/devtools/client/debugger/src/components/Editor/tests/Footer.spec.js
@@ -24,30 +24,30 @@ function generateDefaults(overrides) {
     editor: {
       codeMirror: {
         doc: {},
         cursorActivity: jest.fn(),
         on: jest.fn(),
       },
     },
     endPanelCollapsed: false,
-    selectedSourceWithContent: {
-      source: createSourceObject("foo"),
+    selectedSource: {
+      ...createSourceObject("foo"),
       content: null,
     },
     ...overrides,
   };
 }
 
 function render(overrides = {}, position = { line: 0, column: 0 }) {
   const clear = jest.fn();
   const props = generateDefaults(overrides);
 
   const doc = createMockDocument(clear, position);
-  setDocument(props.selectedSourceWithContent.source.id, doc);
+  setDocument(props.selectedSource.id, doc);
 
   // $FlowIgnore
   const component = shallow(<SourceFooter.WrappedComponent {...props} />, {
     lifecycleExperimental: true,
   });
   return { component, props, clear, doc };
 }
 
--- a/devtools/client/debugger/src/components/PrimaryPanes/Outline.js
+++ b/devtools/client/debugger/src/components/PrimaryPanes/Outline.js
@@ -258,24 +258,22 @@ export class Outline extends Component<P
         </div>
       </div>
     );
   }
 }
 
 const mapStateToProps = state => {
   const selectedSource = getSelectedSourceWithContent(state);
-  const symbols = selectedSource
-    ? getSymbols(state, selectedSource.source)
-    : null;
+  const symbols = selectedSource ? getSymbols(state, selectedSource) : null;
 
   return {
     cx: getContext(state),
     symbols,
-    selectedSource: selectedSource && selectedSource.source,
+    selectedSource: (selectedSource: ?Source),
     selectedLocation: getSelectedLocation(state),
     getFunctionText: line => {
       if (selectedSource) {
         return findFunctionText(line, selectedSource, symbols);
       }
 
       return null;
     },
--- a/devtools/client/debugger/src/reducers/sources.js
+++ b/devtools/client/debugger/src/reducers/sources.js
@@ -797,17 +797,17 @@ function getSourceWithContentInner(
   let contentValue = content[source.id];
 
   let result = contentLookup.get(source);
   if (!result || result.content !== contentValue) {
     if (contentValue && contentValue.state === "pending") {
       contentValue = null;
     }
     result = {
-      source,
+      ...source,
       content: contentValue,
     };
     contentLookup.set(source, result);
   }
 
   return result;
 }
 
--- a/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js
+++ b/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js
@@ -143,36 +143,31 @@ function convertToList(
 ): BreakpointPosition[] {
   return ([].concat(...Object.values(breakpointPositions)): any);
 }
 
 export function getColumnBreakpoints(
   positions: BreakpointPosition[],
   breakpoints: ?(Breakpoint[]),
   viewport: ?Range,
-  selectedSourceWithContent: ?SourceWithContent
+  selectedSource: ?SourceWithContent
 ) {
-  if (!positions || !selectedSourceWithContent) {
+  if (!positions || !selectedSource) {
     return [];
   }
 
-  const {
-    source: selectedSource,
-    content: selectedContent,
-  } = selectedSourceWithContent;
-
   // We only want to show a column breakpoint if several conditions are matched
   // - it is the first breakpoint to appear at an the original location
   // - the position is in the current viewport
   // - there is atleast one other breakpoint on that line
   // - there is a breakpoint on that line
   const breakpointMap = groupBreakpoints(breakpoints, selectedSource);
   positions = filterByLineCount(positions, selectedSource);
   positions = filterVisible(positions, selectedSource, viewport);
-  positions = filterInLine(positions, selectedSource, selectedContent);
+  positions = filterInLine(positions, selectedSource, selectedSource.content);
   positions = filterByBreakpoints(positions, selectedSource, breakpointMap);
 
   return formatPositions(positions, selectedSource, breakpointMap);
 }
 
 const getVisibleBreakpointPositions = createSelector(
   getSelectedSource,
   getBreakpointPositions,
--- a/devtools/client/debugger/src/types.js
+++ b/devtools/client/debugger/src/types.js
@@ -2,18 +2,19 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 
 import type { SettledValue, FulfilledValue } from "./utils/async-value";
 import type { SourcePayload } from "./client/firefox/types";
 import type { SourceActorId, SourceActor } from "./reducers/source-actors";
+import type { SourceBase } from "./reducers/sources";
 
-export type { SourceActorId, SourceActor };
+export type { SourceActorId, SourceActor, SourceBase };
 
 export type SearchModifiers = {
   caseSensitive: boolean,
   wholeWord: boolean,
   regexMatch: boolean,
 };
 
 export type Mode =
@@ -376,24 +377,24 @@ export type TextSourceContent = {|
   contentType: string | void,
 |};
 export type WasmSourceContent = {|
   type: "wasm",
   value: {| binary: Object |},
 |};
 export type SourceContent = TextSourceContent | WasmSourceContent;
 
-export type SourceWithContent = {|
-  source: Source,
+export type SourceWithContent = $ReadOnly<{
+  ...SourceBase,
   +content: SettledValue<SourceContent> | null,
-|};
-export type SourceWithContentAndType<+Content: SourceContent> = {|
-  source: Source,
+}>;
+export type SourceWithContentAndType<+Content: SourceContent> = $ReadOnly<{
+  ...SourceBase,
   +content: FulfilledValue<Content>,
-|};
+}>;
 
 /**
  * Source
  *
  * @memberof types
  * @static
  */
 
--- a/devtools/client/debugger/src/utils/breakpoint/tests/astBreakpointLocation.spec.js
+++ b/devtools/client/debugger/src/utils/breakpoint/tests/astBreakpointLocation.spec.js
@@ -8,19 +8,17 @@ import { getASTLocation } from "../astBr
 import {
   populateSource,
   populateOriginalSource,
 } from "../../../workers/parser/tests/helpers";
 import { getSymbols } from "../../../workers/parser/getSymbols";
 import cases from "jest-in-case";
 
 async function setup({ file, location, functionName, original }) {
-  const { source } = original
-    ? populateOriginalSource(file)
-    : populateSource(file);
+  const source = original ? populateOriginalSource(file) : populateSource(file);
 
   const symbols = getSymbols(source.id);
 
   const astLocation = getASTLocation(source, symbols, location);
   expect(astLocation.name).toBe(functionName);
   expect(astLocation).toMatchSnapshot();
 }
 
--- a/devtools/client/debugger/src/utils/editor/tests/editor.spec.js
+++ b/devtools/client/debugger/src/utils/editor/tests/editor.spec.js
@@ -21,17 +21,17 @@ import {
   getTextForLine,
   getCursorLine,
 } from "../index";
 
 import { makeMockSource, makeMockSourceAndContent } from "../../test-mockup";
 
 describe("shouldShowPrettyPrint", () => {
   it("shows pretty print for a source", () => {
-    const { source, content } = makeMockSourceAndContent(
+    const { content, ...source } = makeMockSourceAndContent(
       "http://example.com/index.js",
       "test-id-123",
       "text/javascript",
       "some text here"
     );
     expect(shouldShowPrettyPrint(source, content)).toEqual(true);
   });
 });
--- a/devtools/client/debugger/src/utils/function.js
+++ b/devtools/client/debugger/src/utils/function.js
@@ -6,39 +6,39 @@
 import { isFulfilled } from "./async-value";
 import { findClosestFunction } from "./ast";
 import { correctIndentation } from "./indentation";
 import type { SourceWithContent } from "../types";
 import type { Symbols } from "../reducers/ast";
 
 export function findFunctionText(
   line: number,
-  { source, content }: SourceWithContent,
+  source: SourceWithContent,
   symbols: ?Symbols
 ): ?string {
   const func = findClosestFunction(symbols, {
     sourceId: source.id,
     line,
     column: Infinity,
   });
 
   if (
     source.isWasm ||
     !func ||
-    !content ||
-    !isFulfilled(content) ||
-    content.value.type !== "text"
+    !source.content ||
+    !isFulfilled(source.content) ||
+    source.content.value.type !== "text"
   ) {
     return null;
   }
 
   const {
     location: { start, end },
   } = func;
-  const lines = content.value.value.split("\n");
+  const lines = source.content.value.value.split("\n");
   const firstLine = lines[start.line - 1].slice(start.column);
   const lastLine = lines[end.line - 1].slice(0, end.column);
   const middle = lines.slice(start.line, end.line - 1);
   const functionText = [firstLine, ...middle, lastLine].join("\n");
   const indentedFunctionText = correctIndentation(functionText);
 
   return indentedFunctionText;
 }
--- a/devtools/client/debugger/src/utils/isMinified.js
+++ b/devtools/client/debugger/src/utils/isMinified.js
@@ -8,26 +8,30 @@ import type { SourceWithContent } from "
 import { isFulfilled } from "./async-value";
 
 // Used to detect minification for automatic pretty printing
 const SAMPLE_SIZE = 50;
 const INDENT_COUNT_THRESHOLD = 5;
 const CHARACTER_LIMIT = 250;
 const _minifiedCache = new Map();
 
-export function isMinified({ source, content }: SourceWithContent) {
+export function isMinified(source: SourceWithContent) {
   if (_minifiedCache.has(source.id)) {
     return _minifiedCache.get(source.id);
   }
 
-  if (!content || !isFulfilled(content) || content.value.type !== "text") {
+  if (
+    !source.content ||
+    !isFulfilled(source.content) ||
+    source.content.value.type !== "text"
+  ) {
     return false;
   }
 
-  let text = content.value.value;
+  let text = source.content.value.value;
 
   let lineEndIndex = 0;
   let lineStartIndex = 0;
   let lines = 0;
   let indentCount = 0;
   let overCharLimit = false;
 
   // Strip comments.
--- a/devtools/client/debugger/src/utils/test-mockup.js
+++ b/devtools/client/debugger/src/utils/test-mockup.js
@@ -51,46 +51,46 @@ function makeMockSourceWithContent(
   url?: string,
   id?: SourceId,
   contentType?: string = "text/javascript",
   text?: string = ""
 ): SourceWithContent {
   const source = makeMockSource(url, id);
 
   return {
-    source,
+    ...source,
     content: text
       ? asyncValue.fulfilled({
           type: "text",
           value: text,
           contentType,
         })
       : null,
   };
 }
 
 function makeMockSourceAndContent(
   url?: string,
   id?: SourceId,
   contentType?: string = "text/javascript",
   text: string = ""
-): { source: Source, content: TextSourceContent } {
+): { ...SourceBase, content: TextSourceContent } {
   const source = makeMockSource(url, id);
 
   return {
-    source,
+    ...source,
     content: {
       type: "text",
       value: text,
       contentType,
     },
   };
 }
 
-function makeMockWasmSource(): Source {
+function makeMockWasmSource(): SourceBase {
   return {
     id: "wasm-source-id",
     url: "url",
     isBlackBoxed: false,
     isPrettyPrinted: false,
     relativeUrl: "url",
     introductionUrl: null,
     introductionType: undefined,
@@ -101,17 +101,17 @@ function makeMockWasmSource(): Source {
 }
 
 function makeMockWasmSourceWithContent(text: {|
   binary: Object,
 |}): SourceWithContentAndType<WasmSourceContent> {
   const source = makeMockWasmSource();
 
   return {
-    source,
+    ...source,
     content: asyncValue.fulfilled({
       type: "wasm",
       value: text,
     }),
   };
 }
 
 function makeMockScope(
--- a/devtools/client/debugger/src/utils/tests/ast.spec.js
+++ b/devtools/client/debugger/src/utils/tests/ast.spec.js
@@ -5,17 +5,17 @@
 // @flow
 
 import { findBestMatchExpression } from "../ast";
 
 import { getSymbols } from "../../workers/parser/getSymbols";
 import { populateSource } from "../../workers/parser/tests/helpers";
 
 describe("find the best expression for the token", () => {
-  const { source } = populateSource("computed-props");
+  const source = populateSource("computed-props");
   const symbols = getSymbols(source.id);
 
   it("should find the identifier", () => {
     const expression = findBestMatchExpression(symbols, {
       line: 1,
       column: 13,
     });
     expect(expression).toMatchSnapshot();
--- a/devtools/client/debugger/src/utils/tests/function.spec.js
+++ b/devtools/client/debugger/src/utils/tests/function.spec.js
@@ -8,56 +8,56 @@ import { findFunctionText } from "../fun
 
 import { getSymbols } from "../../workers/parser/getSymbols";
 import { populateOriginalSource } from "../../workers/parser/tests/helpers";
 
 describe("function", () => {
   describe("findFunctionText", () => {
     it("finds function", () => {
       const source = populateOriginalSource("func");
-      const symbols = getSymbols(source.source.id);
+      const symbols = getSymbols(source.id);
       const text = findFunctionText(14, source, symbols);
       expect(text).toMatchSnapshot();
     });
 
     it("finds function signature", () => {
       const source = populateOriginalSource("func");
-      const symbols = getSymbols(source.source.id);
+      const symbols = getSymbols(source.id);
 
       const text = findFunctionText(13, source, symbols);
       expect(text).toMatchSnapshot();
     });
 
     it("misses function closing brace", () => {
       const source = populateOriginalSource("func");
-      const symbols = getSymbols(source.source.id);
+      const symbols = getSymbols(source.id);
 
       const text = findFunctionText(15, source, symbols);
 
       // TODO: we should try and match the closing bracket.
       expect(text).toEqual(null);
     });
 
     it("finds property function", () => {
       const source = populateOriginalSource("func");
-      const symbols = getSymbols(source.source.id);
+      const symbols = getSymbols(source.id);
 
       const text = findFunctionText(29, source, symbols);
       expect(text).toMatchSnapshot();
     });
 
     it("finds class function", () => {
       const source = populateOriginalSource("func");
-      const symbols = getSymbols(source.source.id);
+      const symbols = getSymbols(source.id);
 
       const text = findFunctionText(33, source, symbols);
       expect(text).toMatchSnapshot();
     });
 
     it("cant find function", () => {
       const source = populateOriginalSource("func");
-      const symbols = getSymbols(source.source.id);
+      const symbols = getSymbols(source.id);
 
       const text = findFunctionText(20, source, symbols);
       expect(text).toEqual(null);
     });
   });
 });
--- a/devtools/client/debugger/src/utils/tests/source.spec.js
+++ b/devtools/client/debugger/src/utils/tests/source.spec.js
@@ -232,57 +232,53 @@ describe("sources", () => {
         getFileURL(makeMockSource(`http://${encodedUnicode.repeat(39)}.html`))
       ).toBe(`…ttp://${unicode.repeat(39)}.html`);
     });
   });
 
   describe("isJavaScript", () => {
     it("is not JavaScript", () => {
       {
-        const { source, content } = makeMockSourceAndContent(
-          "foo.html",
-          undefined,
-          ""
-        );
-        expect(isJavaScript(source, content)).toBe(false);
+        const source = makeMockSourceAndContent("foo.html", undefined, "");
+        expect(isJavaScript(source, source.content)).toBe(false);
       }
       {
-        const { source, content } = makeMockSourceAndContent(
+        const source = makeMockSourceAndContent(
           undefined,
           undefined,
           "text/html"
         );
-        expect(isJavaScript(source, content)).toBe(false);
+        expect(isJavaScript(source, source.content)).toBe(false);
       }
     });
 
     it("is JavaScript", () => {
       {
-        const { source, content } = makeMockSourceAndContent("foo.js");
-        expect(isJavaScript(source, content)).toBe(true);
+        const source = makeMockSourceAndContent("foo.js");
+        expect(isJavaScript(source, source.content)).toBe(true);
       }
       {
-        const { source, content } = makeMockSourceAndContent("bar.jsm");
-        expect(isJavaScript(source, content)).toBe(true);
+        const source = makeMockSourceAndContent("bar.jsm");
+        expect(isJavaScript(source, source.content)).toBe(true);
       }
       {
-        const { source, content } = makeMockSourceAndContent(
+        const source = makeMockSourceAndContent(
           undefined,
           undefined,
           "text/javascript"
         );
-        expect(isJavaScript(source, content)).toBe(true);
+        expect(isJavaScript(source, source.content)).toBe(true);
       }
       {
-        const { source, content } = makeMockSourceAndContent(
+        const source = makeMockSourceAndContent(
           undefined,
           undefined,
           "application/javascript"
         );
-        expect(isJavaScript(source, content)).toBe(true);
+        expect(isJavaScript(source, source.content)).toBe(true);
       }
     });
   });
 
   describe("isThirdParty", () => {
     it("node_modules", () => {
       expect(isThirdParty(makeMockSource("/node_modules/foo.js"))).toBe(true);
     });
@@ -295,186 +291,189 @@ describe("sources", () => {
 
     it("not third party", () => {
       expect(isThirdParty(makeMockSource("/bar/foo.js"))).toBe(false);
     });
   });
 
   describe("getMode", () => {
     it("//@flow", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/javascript",
         "// @flow"
       );
-      expect(getMode(source, content)).toEqual({
+      expect(getMode(source, source.content)).toEqual({
         name: "javascript",
         typescript: true,
       });
     });
 
     it("/* @flow */", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/javascript",
         "   /* @flow */"
       );
-      expect(getMode(source, content)).toEqual({
+      expect(getMode(source, source.content)).toEqual({
         name: "javascript",
         typescript: true,
       });
     });
 
     it("mixed html", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         undefined,
         undefined,
         "",
         " <html"
       );
-      expect(getMode(source, content)).toEqual({ name: "htmlmixed" });
+      expect(getMode(source, source.content)).toEqual({ name: "htmlmixed" });
     });
 
     it("elm", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/x-elm",
         'main = text "Hello, World!"'
       );
-      expect(getMode(source, content)).toEqual({ name: "elm" });
+      expect(getMode(source, source.content)).toEqual({ name: "elm" });
     });
 
     it("returns jsx if contentType jsx is given", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/jsx",
         "<h1></h1>"
       );
-      expect(getMode(source, content)).toEqual({ name: "jsx" });
+      expect(getMode(source, source.content)).toEqual({ name: "jsx" });
     });
 
     it("returns jsx if sourceMetaData says it's a react component", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         undefined,
         undefined,
         "",
         "<h1></h1>"
       );
       expect(
-        getMode(source, content, { ...defaultSymbolDeclarations, hasJsx: true })
+        getMode(source, source.content, {
+          ...defaultSymbolDeclarations,
+          hasJsx: true,
+        })
       ).toEqual({ name: "jsx" });
     });
 
     it("returns jsx if the fileExtension is .jsx", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         "myComponent.jsx",
         undefined,
         "",
         "<h1></h1>"
       );
-      expect(getMode(source, content)).toEqual({ name: "jsx" });
+      expect(getMode(source, source.content)).toEqual({ name: "jsx" });
     });
 
     it("returns text/x-haxe if the file extension is .hx", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         "myComponent.hx",
         undefined,
         "",
         "function foo(){}"
       );
-      expect(getMode(source, content)).toEqual({ name: "text/x-haxe" });
+      expect(getMode(source, source.content)).toEqual({ name: "text/x-haxe" });
     });
 
     it("typescript", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/typescript",
         "function foo(){}"
       );
-      expect(getMode(source, content)).toEqual({
+      expect(getMode(source, source.content)).toEqual({
         name: "javascript",
         typescript: true,
       });
     });
 
     it("typescript-jsx", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/typescript-jsx",
         "<h1></h1>"
       );
-      expect(getMode(source, content).base).toEqual({
+      expect(getMode(source, source.content).base).toEqual({
         name: "javascript",
         typescript: true,
       });
     });
 
     it("cross-platform clojure(script) with reader conditionals", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         "my-clojurescript-source-with-reader-conditionals.cljc",
         undefined,
         "text/x-clojure",
         "(defn str->int [s] " +
           "  #?(:clj  (java.lang.Integer/parseInt s) " +
           "     :cljs (js/parseInt s)))"
       );
-      expect(getMode(source, content)).toEqual({ name: "clojure" });
+      expect(getMode(source, source.content)).toEqual({ name: "clojure" });
     });
 
     it("clojurescript", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         "my-clojurescript-source.cljs",
         undefined,
         "text/x-clojurescript",
         "(+ 1 2 3)"
       );
-      expect(getMode(source, content)).toEqual({ name: "clojure" });
+      expect(getMode(source, source.content)).toEqual({ name: "clojure" });
     });
 
     it("coffeescript", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/coffeescript",
         "x = (a) -> 3"
       );
-      expect(getMode(source, content)).toEqual({ name: "coffeescript" });
+      expect(getMode(source, source.content)).toEqual({ name: "coffeescript" });
     });
 
     it("wasm", () => {
-      const { source, content } = makeMockWasmSourceWithContent({
+      const source = makeMockWasmSourceWithContent({
         binary: "\x00asm\x01\x00\x00\x00",
       });
-      expect(getMode(source, content.value)).toEqual({ name: "text" });
+      expect(getMode(source, source.content.value)).toEqual({ name: "text" });
     });
 
     it("marko", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         "http://localhost.com:7999/increment/sometestfile.marko",
         undefined,
         "does not matter",
         "function foo(){}"
       );
-      expect(getMode(source, content)).toEqual({ name: "javascript" });
+      expect(getMode(source, source.content)).toEqual({ name: "javascript" });
     });
 
     it("es6", () => {
-      const { source, content } = makeMockSourceAndContent(
+      const source = makeMockSourceAndContent(
         "http://localhost.com:7999/increment/sometestfile.es6",
         undefined,
         "does not matter",
         "function foo(){}"
       );
-      expect(getMode(source, content)).toEqual({ name: "javascript" });
+      expect(getMode(source, source.content)).toEqual({ name: "javascript" });
     });
   });
 
   describe("getSourceLineCount", () => {
     it("should give us the amount bytes for wasm source", () => {
       const { content } = makeMockWasmSourceWithContent({
         binary: "\x00asm\x01\x00\x00\x00",
       });
--- a/devtools/client/debugger/src/utils/tests/wasm.spec.js
+++ b/devtools/client/debugger/src/utils/tests/wasm.spec.js
@@ -32,49 +32,49 @@ describe("wasm", () => {
   const SIMPLE_WASM_NOP_OFFSET = 46;
 
   describe("isWasm", () => {
     it("should give us the false when wasm text was not registered", () => {
       const sourceId = "source.0";
       expect(isWasm(sourceId)).toEqual(false);
     });
     it("should give us the true when wasm text was registered", () => {
-      const { source, content } = makeMockWasmSourceWithContent(SIMPLE_WASM);
-      renderWasmText(source.id, content.value);
+      const source = makeMockWasmSourceWithContent(SIMPLE_WASM);
+      renderWasmText(source.id, source.content.value);
       expect(isWasm(source.id)).toEqual(true);
       // clear shall remove
       clearWasmStates();
       expect(isWasm(source.id)).toEqual(false);
     });
   });
 
   describe("renderWasmText", () => {
     it("render simple wasm", () => {
-      const { source, content } = makeMockWasmSourceWithContent(SIMPLE_WASM);
-      const lines = renderWasmText(source.id, content.value);
+      const source = makeMockWasmSourceWithContent(SIMPLE_WASM);
+      const lines = renderWasmText(source.id, source.content.value);
       expect(lines.join("\n")).toEqual(SIMPLE_WASM_TEXT);
       clearWasmStates();
     });
   });
 
   describe("lineToWasmOffset", () => {
     // Test data sanity check: checking if 'nop' is found in the SIMPLE_WASM.
     expect(SIMPLE_WASM.binary[SIMPLE_WASM_NOP_OFFSET]).toEqual("\x01");
 
     it("get simple wasm nop offset", () => {
-      const { source, content } = makeMockWasmSourceWithContent(SIMPLE_WASM);
-      renderWasmText(source.id, content.value);
+      const source = makeMockWasmSourceWithContent(SIMPLE_WASM);
+      renderWasmText(source.id, source.content.value);
       const offset = lineToWasmOffset(source.id, SIMPLE_WASM_NOP_TEXT_LINE);
       expect(offset).toEqual(SIMPLE_WASM_NOP_OFFSET);
       clearWasmStates();
     });
   });
 
   describe("wasmOffsetToLine", () => {
     it("get simple wasm nop line", () => {
-      const { source, content } = makeMockWasmSourceWithContent(SIMPLE_WASM);
-      renderWasmText(source.id, content.value);
+      const source = makeMockWasmSourceWithContent(SIMPLE_WASM);
+      renderWasmText(source.id, source.content.value);
       const line = wasmOffsetToLine(source.id, SIMPLE_WASM_NOP_OFFSET);
       expect(line).toEqual(SIMPLE_WASM_NOP_TEXT_LINE);
       clearWasmStates();
     });
   });
 });
--- a/devtools/client/debugger/src/workers/parser/tests/findOutOfScopeLocations.spec.js
+++ b/devtools/client/debugger/src/workers/parser/tests/findOutOfScopeLocations.spec.js
@@ -15,58 +15,58 @@ function formatLines(actual) {
       ({ start, end }) =>
         `(${start.line}, ${start.column}) -> (${end.line}, ${end.column})`
     )
     .join("\n");
 }
 
 describe("Parser.findOutOfScopeLocations", () => {
   it("should exclude non-enclosing function blocks", () => {
-    const { source } = populateSource("outOfScope");
+    const source = populateSource("outOfScope");
     const actual = findOutOfScopeLocations(source.id, {
       line: 5,
       column: 5,
     });
 
     expect(formatLines(actual)).toMatchSnapshot();
   });
 
   it("should roll up function blocks", () => {
-    const { source } = populateSource("outOfScope");
+    const source = populateSource("outOfScope");
     const actual = findOutOfScopeLocations(source.id, {
       line: 24,
       column: 0,
     });
 
     expect(formatLines(actual)).toMatchSnapshot();
   });
 
   it("should exclude function for locations on declaration", () => {
-    const { source } = populateSource("outOfScope");
+    const source = populateSource("outOfScope");
     const actual = findOutOfScopeLocations(source.id, {
       line: 3,
       column: 12,
     });
 
     expect(formatLines(actual)).toMatchSnapshot();
   });
 
   it("should treat comments as out of scope", () => {
-    const { source } = populateSource("outOfScopeComment");
+    const source = populateSource("outOfScopeComment");
     const actual = findOutOfScopeLocations(source.id, {
       line: 3,
       column: 2,
     });
 
     expect(actual).toEqual([
       { end: { column: 15, line: 1 }, start: { column: 0, line: 1 } },
     ]);
   });
 
   it("should not exclude in-scope inner locations", () => {
-    const { source } = populateSource("outOfScope");
+    const source = populateSource("outOfScope");
     const actual = findOutOfScopeLocations(source.id, {
       line: 61,
       column: 0,
     });
     expect(formatLines(actual)).toMatchSnapshot();
   });
 });
--- a/devtools/client/debugger/src/workers/parser/tests/framework.spec.js
+++ b/devtools/client/debugger/src/workers/parser/tests/framework.spec.js
@@ -6,17 +6,17 @@
 
 import { getSymbols } from "../getSymbols";
 import { populateOriginalSource } from "./helpers";
 import cases from "jest-in-case";
 
 cases(
   "Parser.getFramework",
   ({ name, file, value }) => {
-    const { source } = populateOriginalSource("frameworks/plainJavascript");
+    const source = populateOriginalSource("frameworks/plainJavascript");
     const symbols = getSymbols(source.id);
     expect(symbols.framework).toBeUndefined();
   },
   [
     {
       name: "is undefined when no framework",
       file: "frameworks/plainJavascript",
       value: undefined,
--- a/devtools/client/debugger/src/workers/parser/tests/getScopes.spec.js
+++ b/devtools/client/debugger/src/workers/parser/tests/getScopes.spec.js
@@ -7,17 +7,17 @@
 
 import getScopes from "../getScopes";
 import { populateOriginalSource } from "./helpers";
 import cases from "jest-in-case";
 
 cases(
   "Parser.getScopes",
   ({ name, file, type, locations }) => {
-    const { source } = populateOriginalSource(file, type);
+    const source = populateOriginalSource(file, type);
 
     locations.forEach(([line, column]) => {
       const scopes = getScopes({
         sourceId: source.id,
         line,
         column,
       });
 
--- a/devtools/client/debugger/src/workers/parser/tests/getSymbols.spec.js
+++ b/devtools/client/debugger/src/workers/parser/tests/getSymbols.spec.js
@@ -7,17 +7,17 @@
 
 import { formatSymbols } from "../utils/formatSymbols";
 import { populateSource, populateOriginalSource } from "./helpers";
 import cases from "jest-in-case";
 
 cases(
   "Parser.getSymbols",
   ({ name, file, original, type }) => {
-    const { source } = original
+    const source = original
       ? populateOriginalSource(file, type)
       : populateSource(file, type);
 
     expect(formatSymbols(source.id)).toMatchSnapshot();
   },
   [
     { name: "es6", file: "es6", original: true },
     { name: "func", file: "func", original: true },
--- a/devtools/client/debugger/src/workers/parser/tests/helpers/index.js
+++ b/devtools/client/debugger/src/workers/parser/tests/helpers/index.js
@@ -5,16 +5,17 @@
 // @flow
 
 import fs from "fs";
 import path from "path";
 
 import type {
   Source,
   TextSourceContent,
+  SourceBase,
   SourceWithContent,
 } from "../../../../types";
 import { makeMockSourceAndContent } from "../../../../utils/test-mockup";
 import { setSource } from "../../sources";
 import * as asyncValue from "../../../../utils/async-value";
 
 export function getFixture(name: string, type: string = "js") {
   return fs.readFileSync(
@@ -42,68 +43,68 @@ function getSourceContent(
   return {
     type: "text",
     value: text,
     contentType,
   };
 }
 
 export function getSource(name: string, type?: string): Source {
-  return getSourceWithContent(name, type).source;
+  return getSourceWithContent(name, type);
 }
 
 export function getSourceWithContent(
   name: string,
   type?: string
-): { source: Source, content: TextSourceContent } {
+): { ...SourceBase, content: TextSourceContent } {
   const { value: text, contentType } = getSourceContent(name, type);
 
   return makeMockSourceAndContent(undefined, name, contentType, text);
 }
 
 export function populateSource(name: string, type?: string): SourceWithContent {
-  const { source, content } = getSourceWithContent(name, type);
+  const { content, ...source } = getSourceWithContent(name, type);
   setSource({
     id: source.id,
     text: content.value,
     contentType: content.contentType,
     isWasm: false,
   });
   return {
-    source,
+    ...source,
     content: asyncValue.fulfilled(content),
   };
 }
 
 export function getOriginalSource(name: string, type?: string): Source {
-  return getOriginalSourceWithContent(name, type).source;
+  return getOriginalSourceWithContent(name, type);
 }
 
 export function getOriginalSourceWithContent(
   name: string,
   type?: string
-): { source: Source, content: TextSourceContent } {
+): { ...SourceBase, content: TextSourceContent } {
   const { value: text, contentType } = getSourceContent(name, type);
 
   return makeMockSourceAndContent(
     undefined,
     `${name}/originalSource-1`,
     contentType,
     text
   );
 }
 
 export function populateOriginalSource(
   name: string,
   type?: string
 ): SourceWithContent {
-  const { source, content } = getOriginalSourceWithContent(name, type);
+  const { content, ...source } = getOriginalSourceWithContent(name, type);
   setSource({
     id: source.id,
     text: content.value,
     contentType: content.contentType,
     isWasm: false,
   });
   return {
-    source,
+    ...source,
     content: asyncValue.fulfilled(content),
   };
 }
--- a/devtools/client/debugger/src/workers/parser/tests/steps.spec.js
+++ b/devtools/client/debugger/src/workers/parser/tests/steps.spec.js
@@ -5,56 +5,56 @@
 // @flow
 
 import { getNextStep } from "../steps";
 import { populateSource } from "./helpers";
 
 describe("getNextStep", () => {
   describe("await", () => {
     it("first await call", () => {
-      const { source } = populateSource("async");
+      const source = populateSource("async");
       const pausePosition = { line: 8, column: 2, sourceId: source.id };
       expect(getNextStep(source.id, pausePosition)).toEqual({
         ...pausePosition,
         line: 9,
       });
     });
 
     it("first await call expression", () => {
-      const { source } = populateSource("async");
+      const source = populateSource("async");
       const pausePosition = { line: 8, column: 9, sourceId: source.id };
       expect(getNextStep(source.id, pausePosition)).toEqual({
         ...pausePosition,
         line: 9,
         column: 2,
       });
     });
 
     it("second await call", () => {
-      const { source } = populateSource("async");
+      const source = populateSource("async");
       const pausePosition = { line: 9, column: 2, sourceId: source.id };
       expect(getNextStep(source.id, pausePosition)).toEqual(null);
     });
 
     it("second call expression", () => {
-      const { source } = populateSource("async");
+      const source = populateSource("async");
       const pausePosition = { line: 9, column: 9, sourceId: source.id };
       expect(getNextStep(source.id, pausePosition)).toEqual(null);
     });
   });
 
   describe("yield", () => {
     it("first yield call", () => {
-      const { source } = populateSource("generators");
+      const source = populateSource("generators");
       const pausePosition = { line: 2, column: 2, sourceId: source.id };
       expect(getNextStep(source.id, pausePosition)).toEqual({
         ...pausePosition,
         line: 3,
       });
     });
 
     it("second yield call", () => {
-      const { source } = populateSource("generators");
+      const source = populateSource("generators");
       const pausePosition = { line: 3, column: 2, sourceId: source.id };
       expect(getNextStep(source.id, pausePosition)).toEqual(null);
     });
   });
 });
--- a/devtools/client/debugger/src/workers/parser/utils/tests/ast.spec.js
+++ b/devtools/client/debugger/src/workers/parser/utils/tests/ast.spec.js
@@ -18,26 +18,21 @@ const astKeys = [
   "program",
   "comments",
   "tokens",
 ];
 
 cases(
   "ast.getAst",
   ({ name }) => {
-    const { source, content } = makeMockSourceAndContent(
-      undefined,
-      "foo",
-      name,
-      "2"
-    );
+    const source = makeMockSourceAndContent(undefined, "foo", name, "2");
     setSource({
       id: source.id,
-      text: content.value || "",
-      contentType: content.contentType,
+      text: source.content.value || "",
+      contentType: source.content.contentType,
       isWasm: false,
     });
     const ast = getAst("foo");
     expect(ast && Object.keys(ast)).toEqual(astKeys);
   },
   [
     { name: "text/javascript" },
     { name: "application/javascript" },
--- a/devtools/client/debugger/test/mochitest/helpers.js
+++ b/devtools/client/debugger/test/mochitest/helpers.js
@@ -236,18 +236,18 @@ function waitForSelectedSource(dbg, url)
     getSelectedSourceWithContent,
     hasSymbols,
     getBreakableLines,
   } = dbg.selectors;
 
   return waitForState(
     dbg,
     state => {
-      const { source, content } = getSelectedSourceWithContent() || {};
-      if (!content) {
+      const source = getSelectedSourceWithContent() || {};
+      if (!source.content) {
         return false;
       }
 
       if (!url) {
         return true;
       }
 
       const newSource = findSource(dbg, url, { silent: true });
@@ -313,19 +313,18 @@ function assertPausedLocation(dbg) {
   assertDebugLine(dbg, pauseLine);
 
   ok(isVisibleInEditor(dbg, getCM(dbg).display.gutters), "gutter is visible");
 }
 
 function assertDebugLine(dbg, line) {
   // Check the debug line
   const lineInfo = getCM(dbg).lineInfo(line - 1);
-  const { source, content } =
-    dbg.selectors.getSelectedSourceWithContent() || {};
-  if (source && !content) {
+  const source = dbg.selectors.getSelectedSourceWithContent() || {};
+  if (source && !source.content) {
     const url = source.url;
     ok(
       false,
       `Looks like the source ${url} is still loading. Try adding waitForLoadedSource in the test.`
     );
     return;
   }
 
@@ -510,24 +509,19 @@ function waitForTime(ms) {
 }
 
 function isSelectedFrameSelected(dbg, state) {
   const frame = dbg.selectors.getVisibleSelectedFrame();
 
   // Make sure the source text is completely loaded for the
   // source we are paused in.
   const sourceId = frame.location.sourceId;
-  const { source, content } =
-    dbg.selectors.getSelectedSourceWithContent() || {};
+  const source = dbg.selectors.getSelectedSourceWithContent() || {};
 
-  if (!source) {
-    return false;
-  }
-
-  if (!content) {
+  if (!source || !source.content) {
     return false;
   }
 
   return source.id == sourceId;
 }
 
 /**
  * Clear all the debugger related preferences.
--- a/testing/talos/talos/tests/devtools/addon/content/tests/debugger/debugger-helpers.js
+++ b/testing/talos/talos/tests/devtools/addon/content/tests/debugger/debugger-helpers.js
@@ -173,20 +173,18 @@ function selectSource(dbg, url) {
   dump(`Selecting source: ${url}\n`);
   const line = 1;
   const source = findSource(dbg, url);
   const cx = dbg.selectors.getContext(dbg.getState());
   dbg.actions.selectLocation(cx, { sourceId: source.id, line });
   return waitForState(
     dbg,
     state => {
-      const { source, content } = dbg.selectors.getSelectedSourceWithContent(
-        state
-      );
-      if (!content) {
+      const source = dbg.selectors.getSelectedSourceWithContent(state);
+      if (!source || !source.content) {
         return false;
       }
 
       // wait for symbols -- a flat map of all named variables in a file -- to be calculated.
       // this is a slow process and becomes slower the larger the file is
       return dbg.selectors.hasSymbols(state, source);
     },
     "selected source"