diff --git a/.eslintignore b/.eslintignore index 905d043c49..654be20e05 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ dist/ coverage/ artifacts/ +flow-typed/ commitlint.config.js diff --git a/.flowconfig b/.flowconfig index 940b1114a0..0dbc4ca5a7 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,6 +1,7 @@ [include] [libs] +flow-typed/ [options] module.system=node diff --git a/flow-typed/README.md b/flow-typed/README.md new file mode 100644 index 0000000000..483ae9edcb --- /dev/null +++ b/flow-typed/README.md @@ -0,0 +1,6 @@ +This directory contains flowtyped declarations for the npm modules that do need +valid flow signatures to be able to flow check the modules part of this project. + +The declarations in this directory are not complete definitions of the type defined +in the related modules source code, this declaration files define just as much as +needed for the purpose of flow checking this project source code. diff --git a/flow-typed/chrome-launcher.decl.js b/flow-typed/chrome-launcher.decl.js new file mode 100644 index 0000000000..30ac6caa83 --- /dev/null +++ b/flow-typed/chrome-launcher.decl.js @@ -0,0 +1,23 @@ +// flow-typed signatures for 'chrome-launcher' module. + +declare module "chrome-launcher" { + declare type LaunchOptions = { + enableExtensions: boolean, + chromePath: ?string, + chromeFlags: Array, + startingUrl: ?string, + userDataDir: ?string, + ignoreDefaultFlags: boolean, + }; + + declare class Launcher { + static defaultFlags: () => Array, + process: child_process$ChildProcess, + kill(): Promise, + } + + declare module.exports: { + Launcher: Class, + launch(options?: LaunchOptions): Promise, + } +} diff --git a/flow-typed/firefox-client.decl.js b/flow-typed/firefox-client.decl.js new file mode 100644 index 0000000000..0f2ce95be9 --- /dev/null +++ b/flow-typed/firefox-client.decl.js @@ -0,0 +1,6 @@ +// flow-typed signatures for 'firefox-client' module. + +declare module "@cliqz-oss/firefox-client" { + declare class FirefoxClient extends event$EventEmitter {} + declare module.exports: Class; +} diff --git a/flow-typed/firefox-profile.decl.js b/flow-typed/firefox-profile.decl.js new file mode 100644 index 0000000000..447ac32f38 --- /dev/null +++ b/flow-typed/firefox-profile.decl.js @@ -0,0 +1,39 @@ +// flow-typed signatures for 'firefox-profile' module. + +declare module "firefox-profile" { + declare type ProfileOptions = { + destinationDirectory: string, + } + + declare type ProfileCallback = + (err: ?Error, profile: Profile) => void; + + declare class Finder { + constructor(baseDir: ?string): Finder, + readProfiles(cb: Function): void, + getPath(name: string, cb: (err: ?Error, profilePath: string) => void): void, + + profiles: Array<{ [key:string]: string }>, + + static locateUserDirectory(): string, + } + + declare class FirefoxProfile { + constructor(opts: ?ProfileOptions): FirefoxProfile, + + extensionsDir: string, + profileDir: string, + userPrefs: string, + defaultPreferences: { [key: string]: any }, + + path(): string, + setPreference(pref: string, value: any): void, + updatePreferences(): void, + + static copy({profileDirectory: string}, cb: ProfileCallback): void, + static copyFromUserProfile({name: string}, cb: ProfileCallback): void, + static Finder: Class, + } + + declare module.exports: Class; +} diff --git a/flow-typed/watchpack.decl.js b/flow-typed/watchpack.decl.js new file mode 100644 index 0000000000..cf7eb9cad3 --- /dev/null +++ b/flow-typed/watchpack.decl.js @@ -0,0 +1,9 @@ +// flow-typed signatures for 'watchpack' module. + +declare module "watchpack" { + declare class Watchpack extends event$EventEmitter { + close(): void, + } + + declare module.exports: Class; +} diff --git a/flow-typed/ws.decl.js b/flow-typed/ws.decl.js new file mode 100644 index 0000000000..99cc6bfd81 --- /dev/null +++ b/flow-typed/ws.decl.js @@ -0,0 +1,36 @@ +// flow-typed signatures for 'ws' module. + +declare module "ws" { + declare type ServerOptions = { + host: string, + port: number, + } + + declare type ServerAddressInfo = { + address: string, + port: number, + } + + declare class WebSocket extends events$EventEmitter { + constructor(url: string): WebSocket, + removeEventListener(eventName: string, cb: Function): void, + readyState: "CONNECTING" | "OPEN" | "CLOSING" | "CLOSED", + send(msg: string): void, + close(): void, + + static OPEN: "OPEN", + static CLOSED: "CLOSED", + static CONNECTING: "CONNECTING", + static CLOSING: "CLOSING", + static Server: Class, + } + + declare class Server extends net$Server { + constructor(opts?: ServerOptions, listenCb: Function): Server, + address(): ServerAddressInfo, + clients: Set, + close(closedCb: Function): void, + } + + declare module.exports: Class; +} diff --git a/src/cmd/build.js b/src/cmd/build.js index f70011ff14..0166d72460 100644 --- a/src/cmd/build.js +++ b/src/cmd/build.js @@ -209,7 +209,9 @@ export async function defaultPackageCreator( // Added 'wx' flags to avoid overwriting of existing package. const stream = createWriteStream(extensionPath, {flags: 'wx'}); - stream.write(buffer, () => stream.end()); + stream.write(buffer, () => { + stream.end(); + }); try { await eventToPromise(stream, 'close'); @@ -224,7 +226,9 @@ export async function defaultPackageCreator( } log.info(`Destination exists, overwriting: ${extensionPath}`); const overwriteStream = createWriteStream(extensionPath); - overwriteStream.write(buffer, () => overwriteStream.end()); + overwriteStream.write(buffer, () => { + overwriteStream.end(); + }); await eventToPromise(overwriteStream, 'close'); } diff --git a/src/cmd/docs.js b/src/cmd/docs.js index 3d0c7cbde6..ebf5e75fd0 100644 --- a/src/cmd/docs.js +++ b/src/cmd/docs.js @@ -14,8 +14,8 @@ export type DocsOptions = { openUrl?: typeof open, } -export const url = 'https://developer.mozilla.org/en-US/Add-ons' + - '/WebExtensions/Getting_started_with_web-ext'; +// eslint-disable-next-line max-len +export const url = 'https://extensionworkshop.com/documentation/develop/getting-started-with-web-ext/'; export default function docs( params: DocsParams, {openUrl = open}: DocsOptions = {} diff --git a/src/cmd/sign.js b/src/cmd/sign.js index 44a2f5a66a..ee05c55748 100644 --- a/src/cmd/sign.js +++ b/src/cmd/sign.js @@ -15,7 +15,8 @@ import type {ExtensionManifest} from '../util/manifest'; const log = createLogger(__filename); -const defaultAsyncFsReadFile = fs.readFile.bind(fs); +const defaultAsyncFsReadFile: (string) => Promise = + fs.readFile.bind(fs); export const extensionIdFile = '.web-extension-id'; diff --git a/src/extension-runners/chromium.js b/src/extension-runners/chromium.js index 3ff6d9e74c..4fad5d4c81 100644 --- a/src/extension-runners/chromium.js +++ b/src/extension-runners/chromium.js @@ -43,7 +43,7 @@ const EXCLUDED_CHROME_FLAGS = [ '--mute-audio', ]; -export const DEFAULT_CHROME_FLAGS = ChromeLauncher.defaultFlags() +export const DEFAULT_CHROME_FLAGS: Array = ChromeLauncher.defaultFlags() .filter((flag) => !EXCLUDED_CHROME_FLAGS.includes(flag)); /** @@ -52,10 +52,10 @@ export const DEFAULT_CHROME_FLAGS = ChromeLauncher.defaultFlags() export class ChromiumExtensionRunner { cleanupCallbacks: Set; params: ChromiumExtensionRunnerParams; - chromiumInstance: ChromeLauncher; + chromiumInstance: ?ChromeLauncher; chromiumLaunch: typeof defaultChromiumLaunch; reloadManagerExtension: string; - wss: WebSocket.Server; + wss: ?WebSocket.Server; exiting: boolean; _promiseSetupDone: ?Promise; @@ -73,7 +73,7 @@ export class ChromiumExtensionRunner { /** * Returns the runner name. */ - getName() { + getName(): string { return 'Chromium'; } @@ -241,7 +241,7 @@ export class ChromiumExtensionRunner { }); } - async wssBroadcast(data: Object) { + async wssBroadcast(data: Object): Promise { return new Promise((resolve) => { const clients = this.wss ? new Set(this.wss.clients) : new Set(); @@ -280,7 +280,7 @@ export class ChromiumExtensionRunner { }); } - async createReloadManagerExtension() { + async createReloadManagerExtension(): Promise { const tmpDir = new TempDir(); await tmpDir.create(); this.registerCleanup(() => tmpDir.remove()); @@ -307,6 +307,7 @@ export class ChromiumExtensionRunner { }) ); + // $FlowIgnore: this method is only called right after creating the server and so wss should be defined. const wssInfo = this.wss.address(); const bgPage = `(function bgPage() { @@ -408,7 +409,8 @@ export class ChromiumExtensionRunner { } if (this.wss) { - await new Promise((resolve) => this.wss.close(resolve)); + await new Promise((resolve) => + this.wss ? this.wss.close(resolve) : resolve()); this.wss = null; } diff --git a/src/extension-runners/firefox-android.js b/src/extension-runners/firefox-android.js index d4e5170c6e..9170e3687c 100644 --- a/src/extension-runners/firefox-android.js +++ b/src/extension-runners/firefox-android.js @@ -89,9 +89,9 @@ export type FirefoxAndroidExtensionRunnerParams = {| */ export class FirefoxAndroidExtensionRunner { // Wait 3s before the next unix socket discovery loop. - static unixSocketDiscoveryRetryInterval = 3 * 1000; + static unixSocketDiscoveryRetryInterval: number = 3 * 1000; // Wait for at most 3 minutes before giving up. - static unixSocketDiscoveryMaxTime = 3 * 60 * 1000; + static unixSocketDiscoveryMaxTime: number = 3 * 60 * 1000; params: FirefoxAndroidExtensionRunnerParams; adbUtils: DefaultADBUtils; @@ -168,7 +168,7 @@ export class FirefoxAndroidExtensionRunner { /** * Returns the runner name. */ - getName() { + getName(): string { return 'Firefox Android'; } diff --git a/src/extension-runners/firefox-desktop.js b/src/extension-runners/firefox-desktop.js index a71515bdc0..ddf1d45cee 100644 --- a/src/extension-runners/firefox-desktop.js +++ b/src/extension-runners/firefox-desktop.js @@ -73,7 +73,7 @@ export class FirefoxDesktopExtensionRunner { /** * Returns the runner name. */ - getName() { + getName(): string { return 'Firefox Desktop'; } diff --git a/src/extension-runners/index.js b/src/extension-runners/index.js index 455eeb5036..097c6b37eb 100644 --- a/src/extension-runners/index.js +++ b/src/extension-runners/index.js @@ -45,7 +45,9 @@ export type MultiExtensionRunnerParams = {| desktopNotifications: typeof defaultDesktopNotifications, |}; -export async function createExtensionRunner(config: ExtensionRunnerConfig) { +export async function createExtensionRunner( + config: ExtensionRunnerConfig +): Promise { switch (config.target) { case 'firefox-desktop': { // TODO: use async import instead of require - https://github.com/mozilla/web-ext/issues/1306 @@ -86,7 +88,7 @@ export class MultiExtensionRunner { /** * Returns the runner name. */ - getName() { + getName(): string { return 'Multi Extension Runner'; } diff --git a/src/firefox/index.js b/src/firefox/index.js index 0b4e4cb155..aaa6d32116 100644 --- a/src/firefox/index.js +++ b/src/firefox/index.js @@ -4,8 +4,7 @@ import path from 'path'; import {promisify} from 'util'; import {default as defaultFxRunner} from 'fx-runner'; -import FirefoxProfile, {copyFromUserProfile as defaultUserProfileCopier} - from 'firefox-profile'; +import FirefoxProfile from 'firefox-profile'; import {fs} from 'mz'; import eventToPromise from 'event-to-promise'; @@ -26,7 +25,9 @@ import type {ExtensionManifest} from '../util/manifest'; const log = createLogger(__filename); -const defaultAsyncFsStat = fs.stat.bind(fs); +const defaultAsyncFsStat: typeof fs.stat = fs.stat.bind(fs); + +const defaultUserProfileCopier = FirefoxProfile.copyFromUserProfile; export const defaultFirefoxEnv = { XPCOM_DEBUG_BREAK: 'stack', diff --git a/src/util/artifacts.js b/src/util/artifacts.js index 67e8f8d00d..288a86ad49 100644 --- a/src/util/artifacts.js +++ b/src/util/artifacts.js @@ -7,7 +7,7 @@ import {createLogger} from './logger'; const log = createLogger(__filename); -const defaultAsyncFsAccess = fs.access.bind(fs); +const defaultAsyncFsAccess: typeof fs.access = fs.access.bind(fs); type PrepareArtifactsDirOptions = { asyncMkdirp?: typeof defaultAsyncMkdirp, diff --git a/src/util/logger.js b/src/util/logger.js index 40ad491957..2c183bf7f3 100644 --- a/src/util/logger.js +++ b/src/util/logger.js @@ -91,7 +91,7 @@ export class ConsoleStream { } } -export const consoleStream = new ConsoleStream(); +export const consoleStream: ConsoleStream = new ConsoleStream(); // createLogger types and implementation. diff --git a/src/util/temp-dir.js b/src/util/temp-dir.js index ea4ccf7ea9..04eaebfa6b 100644 --- a/src/util/temp-dir.js +++ b/src/util/temp-dir.js @@ -127,7 +127,7 @@ export class TempDir { /* * Remove the temp directory. */ - remove() { + remove(): Promise | void { if (!this._removeTempDir) { return; } diff --git a/src/util/zip-dir.js b/src/util/zip-dir.js index 694aa5966f..d5bec19c1a 100644 --- a/src/util/zip-dir.js +++ b/src/util/zip-dir.js @@ -3,4 +3,7 @@ import {promisify} from 'util'; import zipDirModule from 'zip-dir'; -export const zipDir = promisify(zipDirModule); +type PromisedZipDir = + (sourceDir: string, { filter(...any): boolean }) => Promise; + +export const zipDir: PromisedZipDir = promisify(zipDirModule); diff --git a/tests/functional/common.js b/tests/functional/common.js index 89fc793ead..e78d6bedee 100644 --- a/tests/functional/common.js +++ b/tests/functional/common.js @@ -11,21 +11,25 @@ import * as tmpDirUtils from '../../src/util/temp-dir'; export const withTempDir = tmpDirUtils.withTempDir; -export const functionalTestsDir = path.resolve(__dirname); -export const projectDir = path.join(functionalTestsDir, '..', '..'); -export const webExt = process.env.TEST_WEB_EXT_BIN ? +export const functionalTestsDir: string = path.resolve(__dirname); +export const projectDir: string = path.join(functionalTestsDir, '..', '..'); +export const webExt: string = process.env.TEST_WEB_EXT_BIN ? path.resolve(process.env.TEST_WEB_EXT_BIN) : path.join(projectDir, 'bin', 'web-ext'); -export const fixturesDir = path.join(functionalTestsDir, '..', 'fixtures'); -export const minimalAddonPath = path.join(fixturesDir, 'minimal-web-ext'); -export const fixtureEsmImport = path.join(fixturesDir, 'import-as-esm'); -export const fixtureCjsRequire = path.join(fixturesDir, 'require-as-cjs'); -export const fakeFirefoxPath = path.join( +export const fixturesDir: string = + path.join(functionalTestsDir, '..', 'fixtures'); +export const minimalAddonPath: string = + path.join(fixturesDir, 'minimal-web-ext'); +export const fixtureEsmImport: string = + path.join(fixturesDir, 'import-as-esm'); +export const fixtureCjsRequire: string = + path.join(fixturesDir, 'require-as-cjs'); +export const fakeFirefoxPath: string = path.join( functionalTestsDir, process.platform === 'win32' ? 'fake-firefox-binary.bat' : 'fake-firefox-binary.js' ); -export const fakeServerPath = path.join( +export const fakeServerPath: string = path.join( functionalTestsDir, 'fake-amo-server.js' ); diff --git a/tests/unit/helpers.js b/tests/unit/helpers.js index b358e6afa4..c74edeeb04 100644 --- a/tests/unit/helpers.js +++ b/tests/unit/helpers.js @@ -46,7 +46,7 @@ export class ZipFile { /** * Close the zip file and wait fd to release. */ - close() { + close(): Promise | null { this._zip.close(); return this._close; } @@ -193,14 +193,14 @@ export function fake( return stub; } -export function createFakeProcess() { - return fake(process, {}, ['EventEmitter', 'stdin']); +export class StubChildProcess extends EventEmitter { + stderr: EventEmitter = new EventEmitter(); + stdout: EventEmitter = new EventEmitter(); + kill: any = sinon.spy(() => {}); } -export class StubChildProcess extends EventEmitter { - stderr = new EventEmitter(); - stdout = new EventEmitter(); - kill = sinon.spy(() => {}); +export function createFakeProcess(): any { + return fake(process, {}, ['EventEmitter', 'stdin']); } /* @@ -217,7 +217,7 @@ type FakeFirefoxClientParams = {| export function fakeFirefoxClient({ requestResult = {}, requestError, makeRequestResult = {}, makeRequestError, -}: FakeFirefoxClientParams = {}) { +}: FakeFirefoxClientParams = {}): any { return { disconnect: sinon.spy(() => {}), request: sinon.spy( @@ -289,7 +289,7 @@ export const basicManifest = { /* * A basic manifest fixture without an applications property. */ -export const manifestWithoutApps = deepcopy(basicManifest); +export const manifestWithoutApps: any = deepcopy(basicManifest); delete manifestWithoutApps.applications; /* @@ -302,16 +302,16 @@ export class FakeExtensionRunner { this.params = params; } - getName() { + getName(): string { return 'Fake Extension Runner'; } async run() {} async exit() {} - async reloadAllExtensions() { + async reloadAllExtensions(): Promise { return []; } - async reloadExtensionBySourceDir(sourceDir: string) { + async reloadExtensionBySourceDir(sourceDir: string): Promise { const runnerName = this.getName(); return [{runnerName, sourceDir}]; } @@ -320,7 +320,7 @@ export class FakeExtensionRunner { export function getFakeFirefox( implementations: Object = {}, port: number = 6005 -) { +): any { const profile = {}; // empty object just to avoid errors. const firefox = () => Promise.resolve(); const allImplementations = { @@ -334,7 +334,7 @@ export function getFakeFirefox( return fake(defaultFirefoxApp, allImplementations); } -export function getFakeRemoteFirefox(implementations: Object = {}) { +export function getFakeRemoteFirefox(implementations: Object = {}): any { return fake(RemoteFirefox.prototype, implementations); } diff --git a/tests/unit/test-cmd/test.run.js b/tests/unit/test-cmd/test.run.js index 90a2c0fdd7..a079628a42 100644 --- a/tests/unit/test-cmd/test.run.js +++ b/tests/unit/test-cmd/test.run.js @@ -58,9 +58,9 @@ function prepareRun(fakeInstallResult) { } describe('run', () => { - let androidRunnerStub: sinon.SinonStub; - let desktopRunnerStub: sinon.SinonStub; - let chromiumRunnerStub: sinon.SinonStub; + let androidRunnerStub: any; + let desktopRunnerStub: any; + let chromiumRunnerStub: any; beforeEach(() => { androidRunnerStub = sinon.stub( diff --git a/tests/unit/test-extension-runners/test.chromium.js b/tests/unit/test-extension-runners/test.chromium.js index 6f6085560e..f85c798e47 100644 --- a/tests/unit/test-extension-runners/test.chromium.js +++ b/tests/unit/test-extension-runners/test.chromium.js @@ -119,6 +119,7 @@ describe('util/extension-runners/chromium', async () => { const runnerInstance = new ChromiumExtensionRunner(params); await runnerInstance.run(); + // $FlowIgnore: allow to call addess even wss property can be undefined. const wssInfo = runnerInstance.wss.address(); const wsURL = `ws://${wssInfo.address}:${wssInfo.port}`; const wsClient = new WebSocket(wsURL); @@ -136,7 +137,7 @@ describe('util/extension-runners/chromium', async () => { const fakeSocket = new EventEmitter(); sinon.spy(fakeSocket, 'on'); - runnerInstance.wss.emit('connection', fakeSocket); + runnerInstance.wss?.emit('connection', fakeSocket); sinon.assert.calledOnce(fakeSocket.on); fakeSocket.emit('error', new Error('Fake wss socket ERROR')); @@ -614,7 +615,7 @@ describe('util/extension-runners/chromium', async () => { ); describe('reloadAllExtensions', () => { - let runnerInstance; + let runnerInstance: ChromiumExtensionRunner; let wsClient: WebSocket; beforeEach(async () => { @@ -624,6 +625,10 @@ describe('util/extension-runners/chromium', async () => { }); const connectClient = async () => { + if (!runnerInstance.wss) { + throw new Error('WebSocker server is not running'); + } + // $FlowIgnore: if runnerInstance.wss would be unexpectedly undefined the test case will fail. const wssInfo = runnerInstance.wss.address(); const wsURL = `ws://${wssInfo.address}:${wssInfo.port}`; wsClient = new WebSocket(wsURL); @@ -633,6 +638,7 @@ describe('util/extension-runners/chromium', async () => { afterEach(async () => { if (wsClient && (wsClient.readyState === WebSocket.OPEN)) { wsClient.close(); + // $FlowIgnore: allow to nullify wsClient even if wsClient signature doesn't allow it. wsClient = null; } await runnerInstance.exit(); @@ -679,6 +685,7 @@ describe('util/extension-runners/chromium', async () => { }); wsClient.close(); }); + // $FlowIgnore: allow to nullify wsClient even if wsClient signature doesn't allow it. wsClient = null; }); diff --git a/tests/unit/test-firefox/test.firefox.js b/tests/unit/test-firefox/test.firefox.js index 691fc5ff4d..2ca6790737 100644 --- a/tests/unit/test-firefox/test.firefox.js +++ b/tests/unit/test-firefox/test.firefox.js @@ -114,6 +114,7 @@ describe('firefox', () => { it('executes the Firefox runner with a given profile', () => { const runner = createFakeFxRunner(); const profile = fakeProfile; + // $FlowIgnore: allow use of fakeProfile as a fake FirefoxProfile instance. return runFirefox({fxRunner: runner, profile}) .then(() => { sinon.assert.called(runner); @@ -659,6 +660,7 @@ describe('firefox', () => { it('creates a finder', () => { const {FxProfile} = prepareProfileFinderTest(); FxProfile.Finder = sinon.spy(FxProfile.Finder); + // $FlowIgnore: allow use of FxProfile as a fake FirefoxProfile class. firefox.defaultCreateProfileFinder({FxProfile}); sinon.assert.calledWith(FxProfile.Finder, sinon.match(undefined)); }); @@ -668,6 +670,7 @@ describe('firefox', () => { FxProfile.Finder = sinon.spy(FxProfile.Finder); const userDirectoryPath = '/non/existent/path'; + // $FlowIgnore: allow use of FxProfile as a fake FirefoxProfile class. firefox.defaultCreateProfileFinder({userDirectoryPath, FxProfile}); sinon.assert.called(FxProfile.Finder); @@ -689,6 +692,7 @@ describe('firefox', () => { const profileFinder = firefox.defaultCreateProfileFinder({ userDirectoryPath, + // $FlowIgnore: allow use of FxProfile as a fake FirefoxProfile class. FxProfile, }); @@ -715,6 +719,7 @@ describe('firefox', () => { const getter = firefox.defaultCreateProfileFinder({ userDirectoryPath, + // $FlowIgnore: allow use of FxProfile as a fake FirefoxProfile class. FxProfile, }); @@ -744,6 +749,7 @@ describe('firefox', () => { const getter = firefox.defaultCreateProfileFinder({ userDirectoryPath, + // $FlowIgnore: allow use of FxProfile as a fake FirefoxProfile class. FxProfile, }); diff --git a/tests/unit/test.web-ext.js b/tests/unit/test.web-ext.js index 35363f671c..e40f6fbb3f 100644 --- a/tests/unit/test.web-ext.js +++ b/tests/unit/test.web-ext.js @@ -18,7 +18,7 @@ describe('webExt', () => { }); describe('exposes commands', () => { - let stub: sinon.Stub; + let stub: any; afterEach(() => { stub.restore(); stub = undefined;