Skip to content
This repository was archived by the owner on Jan 22, 2019. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/config/link.entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
window.EXTENSION_ENV = 'LINK'
1 change: 1 addition & 0 deletions src/config/options.entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
window.EXTENSION_ENV = 'OPTIONS'
11 changes: 11 additions & 0 deletions src/extension/envAssertion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function assertEnv(env: typeof window['EXTENSION_ENV']): void {
if (window.EXTENSION_ENV !== env) {
throw new Error(
'Detected transitive import of an entrypoint! ' +
window.EXTENSION_ENV +
' attempted to import a file that is only intended to be imported by ' +
env +
'.'
)
}
}
78 changes: 29 additions & 49 deletions src/extension/scripts/background.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import storage, { defaultStorageItems } from '../../browser/storage'
import * as tabs from '../../browser/tabs'
import initializeCli from '../../libs/cli'
import { resolveClientConfiguration } from '../../shared/backend/server'
import { isBackground } from '../../shared/context'
import { ExtensionConnectionInfo, onFirstMessage } from '../../shared/messaging'
import { DEFAULT_SOURCEGRAPH_URL, setSourcegraphUrl } from '../../shared/util/context'
import { assertEnv } from '../envAssertion'

assertEnv('BACKGROUND')

let customServerOrigins: string[] = []

Expand Down Expand Up @@ -439,52 +442,29 @@ const spawnAndConnect = ({
connectPortAndWorker(port, worker)
})

/**
* The information necessary to connect to a Sourcegraph extension.
*/
export interface ExtensionConnectionInfo {
extensionID: string
jsBundleURL: string
}

/**
* Executes the callback only on the first message that's received on the port.
*/
export const onFirstMessage = (port: chrome.runtime.Port, callback: (message: any) => void) => {
const cb = message => {
port.onMessage.removeListener(cb)
callback(message)
}
port.onMessage.addListener(cb)
}

// This must not execute anywhere but the background script, otherwise messages
// will get duplicated and cause all kinds of unintuitive behavior.
if (isBackground) {
// This is the bridge between content scripts (that want to connect to Sourcegraph extensions) and the background
// script (that spawns JS bundles or connects to WebSocket endpoints).:
chrome.runtime.onConnect.addListener(port => {
// When a content script wants to create a connection to a Sourcegraph extension, it first connects to the
// background script on a random port and sends a message containing the platform information for that
// Sourcegraph extension (e.g. a JS bundle at localhost:1234/index.js).
onFirstMessage(port, (connectionInfo: ExtensionConnectionInfo) => {
// The background script receives the message and attempts to spawn the
// extension:
spawnAndConnect({ connectionInfo, port }).then(
// If spawning succeeds, the background script sends {} (so the content script knows it succeeded) and
// the port communicates using the internal Sourcegraph extension RPC API after that.
() => {
// Success is represented by the absence of an error
port.postMessage({})
},
// If spawning fails, the background script sends { error } (so the content script knows it failed) and
// the port is immediately disconnected. There is always a 1-1 correspondence between ports and content
// scripts, so this won't disrupt any other connections.
error => {
port.postMessage({ error })
port.disconnect()
}
)
})
// This is the bridge between content scripts (that want to connect to Sourcegraph extensions) and the background
// script (that spawns JS bundles or connects to WebSocket endpoints).:
chrome.runtime.onConnect.addListener(port => {
// When a content script wants to create a connection to a Sourcegraph extension, it first connects to the
// background script on a random port and sends a message containing the platform information for that
// Sourcegraph extension (e.g. a JS bundle at localhost:1234/index.js).
onFirstMessage(port, (connectionInfo: ExtensionConnectionInfo) => {
// The background script receives the message and attempts to spawn the
// extension:
spawnAndConnect({ connectionInfo, port }).then(
// If spawning succeeds, the background script sends {} (so the content script knows it succeeded) and
// the port communicates using the internal Sourcegraph extension RPC API after that.
() => {
// Success is represented by the absence of an error
port.postMessage({})
},
// If spawning fails, the background script sends { error } (so the content script knows it failed) and
// the port is immediately disconnected. There is always a 1-1 correspondence between ports and content
// scripts, so this won't disrupt any other connections.
error => {
port.postMessage({ error })
port.disconnect()
}
)
})
}
})
3 changes: 3 additions & 0 deletions src/extension/scripts/inject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { injectBitbucketServer } from '../../libs/bitbucket/inject'
import { injectGitHubApplication } from '../../libs/github/inject'
import { injectPhabricatorApplication } from '../../libs/phabricator/app'
import { injectSourcegraphApp } from '../../libs/sourcegraph/inject'
import { assertEnv } from '../envAssertion'

assertEnv('CONTENT')

/**
* Main entry point into browser extension.
Expand Down
3 changes: 3 additions & 0 deletions src/extension/scripts/link.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import storage from '../../browser/storage'
import { assertEnv } from '../envAssertion'

assertEnv('LINK')

const searchParams = new URLSearchParams(window.location.search)

Expand Down
3 changes: 3 additions & 0 deletions src/extension/scripts/options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import * as React from 'react'
import { render } from 'react-dom'
import storage from '../../browser/storage'
import { OptionsDashboard } from '../../shared/components/options/OptionsDashboard'
import { assertEnv } from '../envAssertion'

assertEnv('OPTIONS')

const inject = () => {
const injectDOM = document.createElement('div')
Expand Down
2 changes: 1 addition & 1 deletion src/shared/backend/PortMessageTransports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
MessageReader,
MessageWriter,
} from 'sourcegraph/module/protocol/jsonrpc2/transport'
import { ExtensionConnectionInfo } from '../../extension/scripts/background'
import { ExtensionConnectionInfo } from '../messaging'

class PortMessageReader extends AbstractMessageReader implements MessageReader {
private pending: Message[] = []
Expand Down
5 changes: 2 additions & 3 deletions src/shared/backend/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
Settings,
} from '@sourcegraph/extensions-client-common/lib/settings'
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import * as JSONC from '@sqs/jsonc-parser'
import { applyEdits } from '@sqs/jsonc-parser'
import * as JSONC from '@sqs/jsonc-parser'
import { removeProperty, setProperty } from '@sqs/jsonc-parser/lib/edit'
import { isEqual } from 'lodash'
import Alert from 'mdi-react/AlertIcon'
Expand All @@ -25,8 +25,7 @@ import { TextDocumentDecoration } from 'sourcegraph/module/protocol/plainTypes'
import uuid from 'uuid'
import { Disposable } from 'vscode-languageserver'
import storage, { StorageItems } from '../../browser/storage'
import { onFirstMessage } from '../../extension/scripts/background'
import { ExtensionConnectionInfo } from '../../extension/scripts/background'
import { ExtensionConnectionInfo, onFirstMessage } from '../messaging'
import { getContext } from './context'
import { createAggregateError, isErrorLike } from './errors'
import { queryGraphQL } from './graphql'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// We want to polyfill first.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, was this the entry point for the extensions page? Just confused about the name change and don't remember this file before.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might have been the entrypoint a while back, but currently the extensions page is part of the options page, so this entrypoint is unnecessary.

import '../../config/polyfill'
import '../../../config/polyfill'

import * as React from 'react'
import { Button, FormGroup, Input, Label } from 'reactstrap'
import { Subscription } from 'rxjs'
import storage from '../../browser/storage'
import storage from '../../../browser/storage'

interface State {
clientSettings: string
Expand Down
2 changes: 1 addition & 1 deletion src/shared/components/options/ExtensionRegistry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
import * as React from 'react'
import { RouteComponentProps } from 'react-router-dom'
import { Subscription } from 'rxjs'
import { BrowserSettingsEditor } from '../../../extension/scripts/extensions'
import { createExtensionsContextController } from '../../../shared/backend/extensions'
import { GQL } from '../../../types/gqlschema'
import { sourcegraphUrl } from '../../util/context'
import { BrowserSettingsEditor } from './BrowserSettingsEditor'

interface OptionsPageProps extends RouteComponentProps<{}> {}
interface OptionsPageState extends ConfigurationCascadeProps<ConfigurationSubject, Settings> {}
Expand Down
18 changes: 18 additions & 0 deletions src/shared/messaging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* The information necessary to connect to a Sourcegraph extension.
*/
export interface ExtensionConnectionInfo {
extensionID: string
jsBundleURL: string
}

/**
* Executes the callback only on the first message that's received on the port.
*/
export const onFirstMessage = (port: chrome.runtime.Port, callback: (message: any) => void) => {
const cb = message => {
port.onMessage.removeListener(cb)
callback(message)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nifty logic. Wouldn't have thought of that.

}
port.onMessage.addListener(cb)
}
2 changes: 1 addition & 1 deletion src/types/globals/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface Window {
| undefined
SOURCEGRAPH_PHABRICATOR_EXTENSION: boolean | undefined
SG_ENV: 'EXTENSION' | 'PAGE'
EXTENSION_ENV: 'CONTENT' | 'BACKGROUND' | null
EXTENSION_ENV: 'CONTENT' | 'BACKGROUND' | 'OPTIONS' | 'LINK' | null
SOURCEGRAPH_BUNDLE_URL: string | undefined // Bundle Sourcegraph URL is set from the Phabricator extension.
safariMessager?: {
send: (message: { type: string; payload: any }, cb?: (res?: any) => void) => void
Expand Down
7 changes: 4 additions & 3 deletions webpack/base.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const buildEntry = (...files) => files.map(file => path.join(__dirname, file))

const contentEntry = '../src/config/content.entry.js'
const backgroundEntry = '../src/config/background.entry.js'
const linkEntry = '../src/config/link.entry.js'
const optionsEntry = '../src/config/options.entry.js'
const pageEntry = '../src/config/page.entry.js'
const extEntry = '../src/config/extension.entry.js'

Expand All @@ -20,9 +22,8 @@ const babelLoader: webpack.RuleSetUseItem = {
export default {
entry: {
background: buildEntry(extEntry, backgroundEntry, '../src/extension/scripts/background.tsx'),
link: buildEntry(extEntry, contentEntry, '../src/extension/scripts/link.tsx'),
options: buildEntry(extEntry, backgroundEntry, '../src/extension/scripts/options.tsx'),
extensions: buildEntry(extEntry, backgroundEntry, '../src/extension/scripts/extensions.tsx'),
link: buildEntry(extEntry, linkEntry, '../src/extension/scripts/link.tsx'),
options: buildEntry(extEntry, optionsEntry, '../src/extension/scripts/options.tsx'),
inject: buildEntry(extEntry, contentEntry, '../src/extension/scripts/inject.tsx'),
phabricator: buildEntry(pageEntry, '../src/libs/phabricator/extension.tsx'),

Expand Down