├── .babelrc ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── dev-env ├── build.js ├── config.js ├── dev.js ├── manifest │ ├── index.js │ ├── log.js │ ├── plugin.js │ ├── processor │ │ ├── action.js │ │ ├── assets.js │ │ ├── background.js │ │ ├── content.js │ │ ├── csp.js │ │ ├── lib │ │ │ ├── html.js │ │ │ └── script.js │ │ ├── overrides.js │ │ └── package_json.js │ └── processors.js ├── override │ ├── index.js │ └── template │ │ ├── JsonpMainTemplate.runtime.js │ │ └── log-apply-results.js ├── paths.js ├── release ├── util │ └── remove.js └── webpack │ ├── build.js │ ├── config.js │ └── server.js ├── package.json ├── resources └── command.gif ├── src ├── background │ ├── analytics.js │ ├── headers.js │ ├── index.js │ └── vendor │ │ └── google-analytics-bundle.js ├── content │ ├── commands │ │ ├── Emoji │ │ │ ├── Emoji.jsx │ │ │ ├── Emoji.png │ │ │ ├── Emoji.scss │ │ │ └── icons │ │ │ │ ├── activity.png │ │ │ │ ├── animals_and_nature.png │ │ │ │ ├── flags.png │ │ │ │ ├── food_and_drink.png │ │ │ │ ├── objects.png │ │ │ │ ├── people.png │ │ │ │ ├── symbols.png │ │ │ │ └── travel_and_places.png │ │ ├── Giphy │ │ │ ├── Giphy.jsx │ │ │ ├── Giphy.png │ │ │ └── Giphy.scss │ │ ├── Help │ │ │ ├── Help.js │ │ │ └── Help.png │ │ ├── Selfie │ │ │ ├── Selfie.jsx │ │ │ ├── Selfie.png │ │ │ └── Selfie.scss │ │ ├── Spotify │ │ │ ├── Spotify.jsx │ │ │ ├── Spotify.png │ │ │ └── Spotify.scss │ │ ├── index.js │ │ └── mount.js │ ├── components │ │ ├── Container.jsx │ │ ├── Container.scss │ │ ├── Search.jsx │ │ ├── Search.scss │ │ └── index.js │ ├── fields │ │ ├── ContentEditable.js │ │ ├── ContentEditableFacebook.js │ │ ├── ContentEditableHTML.js │ │ ├── ContentEditableMarkdown.js │ │ ├── ContentEditableUnformatted.js │ │ ├── Field.js │ │ ├── HTML.js │ │ ├── Markdown.js │ │ ├── Textarea.js │ │ ├── TextareaMarkdown.js │ │ ├── TextareaUnformatted.js │ │ ├── Unformatted.js │ │ ├── Unsupported.js │ │ ├── index.js │ │ └── support │ │ │ ├── classes.js │ │ │ └── domains.js │ ├── index.js │ ├── lib │ │ ├── analytics.js │ │ ├── at.js │ │ ├── extension.js │ │ ├── imgur.js │ │ └── mixin.js │ ├── styles │ │ └── _settings.scss │ ├── types │ │ ├── Image.js │ │ ├── Link.js │ │ ├── Redirect.js │ │ └── index.js │ └── vendor │ │ ├── jquery.atwho.js │ │ ├── jquery.atwho.scss │ │ ├── jquery.caret.js │ │ └── jquery.rangyinputs.js ├── icons │ ├── command-128.png │ ├── command-16.png │ ├── command-32.png │ ├── command.png │ ├── command@2x.png │ └── command@3x.png └── manifest.json └── test └── index.html /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015" 5 | ], 6 | "env": { 7 | "development": { 8 | "presets": [ 9 | "react-hmre" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | release/ 4 | npm-debug.log 5 | .env 6 | assets 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Welcome! We're so excited to have you, no matter who you are. To make sure we're creating a safe space for everyone, please check out our [Code of Conduct](CODE_OF_CONDUCT.md). 4 | 5 | ## Getting setup 6 | 7 | Install the necessary node modules and start the development server. 8 | 9 | ```bash 10 | $ npm install 11 | $ npm run dev 12 | ``` 13 | 14 | Add the development build of the extension to your browser. 15 | 16 | 1. Navigate to `chrome://extensions/` in Chrome 17 | 2. Check *Developer Mode* 18 | 3. Click on *Load unpacked extension* 19 | 4. Add `command/build` 20 | 21 | Now, navigate to any page and open up your JS console. If there is an SSL error, right click the `localhost` link with the SSL error, open in a new tab, then confirm that you'd like to proceed. This happens because we used a self-signed SSL certificate to serve assets in development 22 | 23 | ## Creating a command 24 | 25 | Creating a new `command` is easy. It requires creating a new command in the `commands` folder. To demonstrate how this works, let's implement a simple command that opens a new tab to `google.com`. 26 | 27 | ### Creating the command file 28 | 29 | The first thing we need to do is create a new command folder. For the new `/google` command we are trying to create, let's add a new folder at `src/commands/Google` and a new file in that folder called `src/commands/Google/Google.js`. Now, let's make that file export the necessary API to implement a command. 30 | 31 | ```javascript 32 | // src/commands/Google/Google.js 33 | 34 | import * as types from 'types' 35 | 36 | export let match = "google" 37 | export let icon = require("./Google.png") 38 | export let mount = (field, onDone) => { 39 | return onDone(new types.Redirect({ 40 | url: 'https://google.com', 41 | target: '_blank' 42 | }) 43 | } 44 | ``` 45 | 46 | Now that we have our file created, let's walk through what exactly we've exported in this module. 47 | 48 | * `match` - this is the name of the command that a user must type to activate it. So, for this command, a user has to type `/google` in order to activate the `Google` command. 49 | * `icon` - this is a `required` icon in PNG form that will be used in the quick-select dropdown when a user starts typing a command. 50 | * `mount` - this is a function which will be called when the command is selected. It is called with two arguments: 51 | * `field` - this is a reference to a `Field` object where the user actually typed the command. For the most part, you won't need to do anything with this: we've created a bunch of easy return `types` that will let you accomplish almost anything without touching the field in question. 52 | * `onDone` - this is the callback the command should call when it's done completing its desired action. This `onDone` function can be optionally called with a `type` object that declares a generic result that should either be applied to the field or happen in the browser. To learn more about the different types available, check out the source at [src/types/](src/types). 53 | * `onInsert` - this is a callback that can be used to insert before we call `onDone`. Like `onDone`, this should be called with a `type` object that declares a generic result to be applied to the field or happen in the browser. To learn more about the different types available, check out the source at [src/types/](src/types). 54 | 55 | ### Enabling the command 56 | 57 | Once you've created the folder and exported the required exports, there's nothing else to do! The command should now automatically be loaded into your browser. 58 | 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jesse Pollak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Command ([website](http://slashcommand.club)) 2 | 3 | Making the web better with Slack-like slash commands. 4 | 5 | Check out [slashcommand.club](http://slashcommand.club) for more a demo, examples and installation. 6 | 7 |  8 | 9 | ## Usage 10 | 11 | 1. Install the Command extension [here](https://chrome.google.com/webstore/detail/command/dkblejpmbmienbjpinbgebodokhpbkme) 12 | 2. Type a slash command like `/giphy` in any text field 13 | 3. Win! 14 | 15 | Now, would you like to give it a try? 16 | 17 | 1. Go to the [welcome Github issue](https://github.com/jessepollak/command/issues/1) 18 | 2. Click on the comment field at the bottom, say something witty, then type `/giphy` 19 | 3. Add the perfect GIF 20 | 21 | ## Current commands 22 | 23 | * */emoji* - add emoji 24 | * */giphy* - search and add a GIF 25 | * */help* - learn how to use Command 26 | * */selfie* - take and embed a selfie from your webcam 27 | * */spotify* - search and share songs from spotify 28 | 29 | ## Contributing 30 | 31 | To contribute, check out our [Contributors Guide](CONTRIBUTING.md). 32 | 33 | ## Credits 34 | 35 | Thanks to [@schovi](https://github.com/schovi) for creating [webpack-chrome-extension](https://github.com/schovi/webpack-chrome-extension). 36 | -------------------------------------------------------------------------------- /dev-env/build.js: -------------------------------------------------------------------------------- 1 | // Native 2 | import fs from 'fs-extra'; 3 | import { exec } from 'child_process' 4 | import archiver from 'archiver' 5 | 6 | // npm 7 | import clc from 'cli-color'; 8 | 9 | // package 10 | import makeWebpackConfig from './webpack/config'; 11 | import webpackBuild from './webpack/build'; 12 | import Manifest from './manifest' 13 | import * as paths from './paths' 14 | 15 | // Clear release direcotry 16 | fs.removeSync(paths.release) 17 | fs.mkdirsSync(paths.release) 18 | 19 | // Create manifest 20 | const manifest = new Manifest({manifest: paths.manifest, build: paths.build}) 21 | manifest.run() 22 | 23 | // Build webpack 24 | const webpackConfig = makeWebpackConfig(manifest) 25 | const building = webpackBuild(webpackConfig) 26 | 27 | building.then(() => { 28 | console.log(clc.green("\n-- Building done --\n")) 29 | 30 | // Build extension 31 | // TODO try detect system and Chrome path. Default is OSX :) 32 | const chromeBinaryPath = process.env.CHROME_BIN || '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome' 33 | 34 | console.log(clc.yellow("Packaging zip")) 35 | 36 | var output = fs.createWriteStream('release/build.zip') 37 | var archive = archiver('zip') 38 | 39 | archive.pipe(output) 40 | archive.bulk([ 41 | { expand: true, cwd: 'release/build', src: ['**'], dest: 'build'} 42 | ]) 43 | archive.finalize() 44 | console.log(clc.green("Done")) 45 | 46 | console.log(clc.yellow(`Packing extension into '${paths.build}'`)) 47 | exec(`\$('${chromeBinaryPath}' --pack-extension=${paths.build})`, (error, stdout, stderr) => { 48 | console.log(clc.green('Done')); 49 | 50 | if(stdout) 51 | console.log(clc.yellow('stdout: ' + stdout)); 52 | 53 | if(stderr) 54 | console.log(clc.red('stderr: ' + stderr)); 55 | 56 | if(error !== null) 57 | console.log(clc.red('exec error: ' + error)); 58 | }) 59 | }).catch((reason) => { 60 | console.error(clc.red("Building failed")) 61 | console.error(clc.red(reason.stack)) 62 | }) 63 | -------------------------------------------------------------------------------- /dev-env/config.js: -------------------------------------------------------------------------------- 1 | export let PORT = process.env.PORT || 3001 2 | export let DEV_URL = `https://localhost:${PORT}` 3 | -------------------------------------------------------------------------------- /dev-env/dev.js: -------------------------------------------------------------------------------- 1 | import makeWebpackConfig from './webpack/config'; 2 | import webpackDevServer from './webpack/server'; 3 | import overrideHotUpdater from './override' 4 | import Manifest from './manifest' 5 | import * as paths from './paths' 6 | 7 | // Override Webpack hot updater 8 | overrideHotUpdater() 9 | 10 | // Create manifest 11 | const manifest = new Manifest({manifest: paths.manifest, build: paths.build}) 12 | manifest.run() 13 | 14 | // Start webpack dev server 15 | const webpackConfig = makeWebpackConfig(manifest) 16 | webpackDevServer(webpackConfig) 17 | -------------------------------------------------------------------------------- /dev-env/manifest/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | // import chokidar from 'chokidar' 4 | 5 | import processors from './processors' 6 | import * as log from './log' 7 | 8 | export default class Manifest { 9 | constructor({ manifest, build }) { 10 | this.manifestPath = manifest 11 | this.buildPath = build 12 | } 13 | 14 | run() { 15 | this.prepareBuildDir() 16 | this.processManifest() 17 | this.writeManifest() 18 | } 19 | 20 | // Start as plugin in webpack 21 | apply() { 22 | this.run() 23 | // this.watch() 24 | } 25 | 26 | // watch() { 27 | // chokidar.watch(this.path).on('change', this.onChange) 28 | // } 29 | 30 | // onChange = (event, path) => { 31 | // this.processManifest() 32 | // } 33 | 34 | prepareBuildDir() { 35 | // Prepare clear build 36 | fs.removeSync(this.buildPath) 37 | fs.mkdirsSync(this.buildPath) 38 | } 39 | 40 | writeManifest() { 41 | const manifestPath = path.join(this.buildPath, "manifest.json"); 42 | log.pending(`Making 'build/manifest.json'`) 43 | fs.writeFileSync(manifestPath, JSON.stringify(this.manifest, null, 2), {encoding: 'utf8'}) 44 | log.done() 45 | } 46 | 47 | loadManifest() { 48 | return JSON.parse(fs.readFileSync(this.manifestPath, 'utf8')) 49 | } 50 | 51 | processManifest() { 52 | this.scripts = [] 53 | this.manifest = this.loadManifest() 54 | 55 | // Iterate over each processor and process manifest with it 56 | processors.forEach((processor) => { 57 | this.applyProcessorResult( 58 | processor(this.manifest, this) 59 | ) 60 | }) 61 | 62 | return true 63 | } 64 | 65 | applyProcessorResult({manifest, scripts} = {}) { 66 | if(manifest) 67 | this.manifest = manifest 68 | 69 | if(scripts) { 70 | // TODO validace na skripty 71 | // const pushScriptName = function(scriptName) { 72 | // const scriptPath = path.join(paths.src, scriptName) 73 | // 74 | // if(!fs.existsSync(scriptPath)) { 75 | // console.warn(clc.red(`Missing script ${scriptPath}`)) 76 | // 77 | // return 78 | // } 79 | // 80 | // if(~scripts.indexOf(scriptName)) 81 | // return 82 | // 83 | // scripts.push(scriptName) 84 | // } 85 | 86 | scripts.forEach((script) => { 87 | this.scripts.push(script) 88 | }) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /dev-env/manifest/log.js: -------------------------------------------------------------------------------- 1 | import clc from 'cli-color'; 2 | 3 | export const pending = function(message) { 4 | console.log(clc.yellow(message)) 5 | } 6 | 7 | export const success = function(message) { 8 | console.log(clc.green(message)) 9 | } 10 | 11 | export const error = function(message) { 12 | console.error(clc.red(message)) 13 | } 14 | 15 | export const done = function() { 16 | success("Done") 17 | } 18 | -------------------------------------------------------------------------------- /dev-env/manifest/plugin.js: -------------------------------------------------------------------------------- 1 | import SingleEntryPlugin from "webpack/lib/SingleEntryPlugin" 2 | import MultiEntryPlugin from "webpack/lib/MultiEntryPlugin" 3 | import * as Remove from '../util/remove' 4 | import * as config from '../config' 5 | 6 | export default class ManifestPlugin { 7 | constructor(Manifest) { 8 | this.Manifest = Manifest 9 | this.isDevelopment = process.env.NODE_ENV != "production" 10 | } 11 | 12 | apply(compiler) { 13 | this.Manifest.scripts.forEach((script) => { 14 | // name 15 | const name = Remove.extension(script) 16 | 17 | // item 18 | let item 19 | if(this.isDevelopment) { 20 | item = [ 21 | `webpack-dev-server/client?${config.DEV_URL}`, 22 | 'webpack/hot/only-dev-server', 23 | script 24 | ] 25 | } else { 26 | item = script 27 | } 28 | 29 | const entryClass = this.itemToPlugin(item, name) 30 | 31 | compiler.apply(entryClass) 32 | }) 33 | } 34 | 35 | itemToPlugin(item, name) { 36 | if(Array.isArray(item)) { 37 | return new MultiEntryPlugin(null, item, name); 38 | } else { 39 | return new SingleEntryPlugin(null, item, name); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /dev-env/manifest/processor/action.js: -------------------------------------------------------------------------------- 1 | import html from './lib/html' 2 | 3 | const process = function({action: {default_popup} = {}, buildPath, scripts}) { 4 | if(!default_popup) return 5 | 6 | scripts.push(html(default_popup, buildPath)) 7 | 8 | return true 9 | } 10 | 11 | export default function(manifest, {buildPath}) { 12 | 13 | const {browser_action, page_action} = manifest 14 | 15 | const scripts = [] 16 | 17 | // Browser action 18 | process({action: browser_action, buildPath, scripts}) 19 | 20 | // Page action 21 | process({action: page_action, buildPath, scripts}) 22 | 23 | return {scripts} 24 | } 25 | -------------------------------------------------------------------------------- /dev-env/manifest/processor/assets.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | import _ from 'lodash' 4 | 5 | import * as paths from '../../paths' 6 | import * as log from '../log' 7 | import * as Remove from '../../util/remove'; 8 | 9 | const buildAssetsDir = "$assets" 10 | 11 | const processAsset = function(object, key, buildPath) { 12 | const assetPath = object[key] 13 | 14 | log.pending(`Processing asset '${assetPath}'`) 15 | 16 | // Create directory if not exists 17 | const buildAssetsDirPath = path.join(buildPath, buildAssetsDir) 18 | try { 19 | const buildAssetsDirStats = fs.lstatSync(buildAssetsDirPath); 20 | 21 | if(!buildAssetsDirStats.isDirectory()) { 22 | fs.mkdirsSync(buildAssetsDirPath) 23 | } 24 | } catch(ex) { 25 | fs.mkdirsSync(buildAssetsDirPath) 26 | } 27 | 28 | const assetSrcPath = path.join(paths.src, assetPath) 29 | const buildAssetPath = path.join(buildAssetsDir, Remove.path(assetPath)) 30 | const assetDestPath = path.join(buildPath, buildAssetPath) 31 | 32 | fs.copySync(assetSrcPath, assetDestPath) 33 | 34 | object[key] = buildAssetPath 35 | 36 | log.done(`Done`) 37 | 38 | return true 39 | } 40 | 41 | export default function(manifest, {buildPath}) { 42 | 43 | // Process icons 44 | if (manifest.icons && Object.keys(manifest.icons).length) { 45 | _.forEach(manifest.icons, (iconPath, name) => processAsset(manifest.icons, name, buildPath)) 46 | } 47 | 48 | // TODO can there be more assets? 49 | 50 | return {manifest} 51 | } 52 | -------------------------------------------------------------------------------- /dev-env/manifest/processor/background.js: -------------------------------------------------------------------------------- 1 | import script from './lib/script' 2 | import html from './lib/html' 3 | 4 | export default function(manifest, {buildPath}) { 5 | const {background} = manifest 6 | 7 | // Skip when there is no background property 8 | if(!background) 9 | return 10 | 11 | const scripts = [] 12 | 13 | // Process background scripts 14 | if(background.scripts) { 15 | background.scripts.forEach((scriptPath) => { 16 | script(scriptPath, buildPath) 17 | scripts.push(scriptPath) 18 | }) 19 | } 20 | 21 | // Background page 22 | if(background.page) { 23 | scripts.push(html(background.page, buildPath)) 24 | } 25 | 26 | return {manifest, scripts} 27 | } 28 | -------------------------------------------------------------------------------- /dev-env/manifest/processor/content.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import script from './lib/script' 4 | 5 | export default function(manifest, {buildPath}) { 6 | const {content_scripts} = manifest 7 | 8 | if(!content_scripts) return 9 | 10 | const scripts = [] 11 | 12 | _.each(content_scripts, (content_script) => { 13 | // TODO content_script can contain css too. 14 | // Maybe we can be strict, throw error and tell user to add css into scripts and leave it on webpack too 15 | _.each(content_script.js, (scriptPath) => { 16 | script(scriptPath, buildPath) 17 | scripts.push(scriptPath) 18 | }) 19 | }) 20 | 21 | return {scripts} 22 | } 23 | -------------------------------------------------------------------------------- /dev-env/manifest/processor/csp.js: -------------------------------------------------------------------------------- 1 | import * as log from '../log' 2 | import * as config from '../../config' 3 | 4 | ////////// 5 | // CSP. Fix Content security policy to allow eval webpack scripts in development mode 6 | export default function(manifest) { 7 | log.pending("Processing CSP") 8 | 9 | if(process.env.NODE_ENV == 'development') { 10 | 11 | let csp = manifest["content_security_policy"] || "" 12 | 13 | const objectSrc = "object-src 'self'" 14 | 15 | if(~csp.indexOf('object-src')) { 16 | csp = csp.replace('object-src', objectSrc) 17 | } else { 18 | csp = `${objectSrc}; ${csp}` 19 | } 20 | 21 | // TODO add host into some config 22 | const scriptSrc = `script-src 'self' 'unsafe-eval' ${config.DEV_URL}` 23 | 24 | if(~csp.indexOf('script-src')) { 25 | csp = csp.replace('script-src', scriptSrc) 26 | } else { 27 | csp = `${scriptSrc}; ${csp}` 28 | } 29 | 30 | manifest["content_security_policy"] = csp 31 | 32 | log.done("Done") 33 | } else { 34 | log.done("Skipped in production environment") 35 | } 36 | 37 | return {manifest} 38 | } 39 | -------------------------------------------------------------------------------- /dev-env/manifest/processor/lib/html.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | 4 | import * as config from '../../../config' 5 | import * as log from '../../log' 6 | import script from './script' 7 | import * as Remove from '../../../util/remove' 8 | 9 | const makeLayout = function({script, body}) { 10 | return ( 11 | ` 12 | 13 |
14 | 15 | 16 | 17 | 18 | ${body} 19 | ${script} 20 | 21 | ` 22 | ) 23 | } 24 | 25 | export default function(htmlFilepath, buildPath) { 26 | log.pending(`Making html '${htmlFilepath}'`) 27 | 28 | // Read body content 29 | const htmlContent = fs.readFileSync(path.resolve(path.join('src', htmlFilepath)), {encoding: "utf8"}) 30 | 31 | // Get just path and name ie: 'popup/index' 32 | const bareFilepath = Remove.extension(htmlFilepath) 33 | 34 | const scriptFilepath = `${bareFilepath}.js` 35 | 36 | const webpackScriptUrl = process.env.NODE_ENV == "development" ? path.join(config.DEV_URL, scriptFilepath) : `/${scriptFilepath}` 37 | const webpackScript = ``; 38 | 39 | script(scriptFilepath, buildPath) 40 | 41 | const html = makeLayout({ 42 | body: htmlContent, 43 | script: webpackScript 44 | }) 45 | 46 | const fullHtmlPath = path.join(buildPath, htmlFilepath) 47 | 48 | fs.mkdirsSync(Remove.file(fullHtmlPath)) 49 | 50 | fs.writeFileSync(fullHtmlPath, html) 51 | 52 | log.done() 53 | 54 | return scriptFilepath 55 | } 56 | -------------------------------------------------------------------------------- /dev-env/manifest/processor/lib/script.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | 4 | import * as config from '../../../config' 5 | import * as log from '../../log' 6 | import * as Remove from '../../../util/remove' 7 | 8 | const makeInjector = function(scriptName) { 9 | return ( 10 | `// Injector file for '${scriptName}' 11 | var context = this; 12 | 13 | // http://stackoverflow.com/questions/8403108/calling-eval-in-particular-context/25859853#25859853 14 | function evalInContext(js, context) { 15 | return function() { return eval(js); }.call(context); 16 | } 17 | 18 | function reqListener () { 19 | evalInContext(this.responseText, context) 20 | } 21 | 22 | var request = new XMLHttpRequest(); 23 | request.onload = reqListener; 24 | request.open("get", "${config.DEV_URL}/${scriptName}", true); 25 | request.send();` 26 | ) 27 | } 28 | 29 | export default function(scriptName, buildPath) { 30 | if(process.env.NODE_ENV == 'development') { 31 | log.pending(`Making injector '${scriptName}'`) 32 | 33 | const injectorScript = makeInjector(scriptName); 34 | const injectorFilepath = path.join(buildPath, scriptName); 35 | const injectorPath = Remove.file(injectorFilepath) 36 | 37 | fs.mkdirsSync(injectorPath) 38 | fs.writeFileSync(injectorFilepath, injectorScript, {encoding: 'utf8'}) 39 | 40 | log.done() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /dev-env/manifest/processor/overrides.js: -------------------------------------------------------------------------------- 1 | import html from './lib/html' 2 | 3 | const process = function({page, buildPath, scripts}) { 4 | if(!page) return 5 | 6 | scripts.push(html(page, buildPath)) 7 | 8 | return true 9 | } 10 | 11 | export default function(manifest, {buildPath}) { 12 | 13 | if(!manifest.chrome_url_overrides) 14 | return 15 | 16 | const {bookmarks, history, newtab} = manifest.chrome_url_overrides 17 | 18 | const scripts = [] 19 | 20 | // Bookmarks page 21 | process({page: bookmarks, buildPath, scripts}) 22 | 23 | // History page 24 | process({page: history, buildPath, scripts}) 25 | 26 | // New tab page 27 | process({page: newtab, buildPath, scripts}) 28 | 29 | return {scripts} 30 | } 31 | -------------------------------------------------------------------------------- /dev-env/manifest/processor/package_json.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import _ from 'lodash' 3 | 4 | import * as paths from '../../paths' 5 | 6 | ////////// 7 | // Merge manifest.json with name, description and version from package.json 8 | export default function(manifest) { 9 | const packageConfig = JSON.parse(fs.readFileSync(paths.packageJson, 'utf8')) 10 | 11 | manifest = _.merge({}, manifest, _.pick(packageConfig, 'name', 'description', 'version')); 12 | 13 | return {manifest} 14 | } 15 | -------------------------------------------------------------------------------- /dev-env/manifest/processors.js: -------------------------------------------------------------------------------- 1 | import Csp from './processor/csp' 2 | import PackageJson from './processor/package_json' 3 | import Assets from './processor/assets' 4 | import Action from './processor/action' 5 | import Background from './processor/background' 6 | import Content from './processor/content' 7 | import Overrides from './processor/overrides' 8 | 9 | 10 | const processors = [ 11 | // Fix csp for devel 12 | Csp, 13 | // Mege package.json 14 | PackageJson, 15 | // Process assets 16 | Assets, 17 | // Process action (browse, or page) 18 | Action, 19 | // Process background script 20 | Background, 21 | // Process content script 22 | Content, 23 | // Process overrides 24 | Overrides 25 | ] 26 | 27 | export default processors 28 | -------------------------------------------------------------------------------- /dev-env/override/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import clc from 'cli-color'; 4 | 5 | export default function() { 6 | 7 | // HACK 8 | // Override Webpack HOT code loader with my custom one. 9 | // Hot update is loaded via XMLHttpRequest and evaled in extension 10 | // context instead of including script tag with that hot update 11 | 12 | const originalJsonpMainTemplatePath = require.resolve(path.join(__dirname, '../../node_modules/webpack/lib/JsonpMainTemplate.runtime.js')) 13 | const overridenJsonpMainTemplatePath = require.resolve(path.join(__dirname, './template/JsonpMainTemplate.runtime.js')) 14 | const overridenJsonpMainTemplate = fs.readFileSync(overridenJsonpMainTemplatePath, {encoding: "utf8"}) 15 | 16 | console.log(clc.green("Overriding 'node_modules/webpack/lib/JsonpMainTemplate.runtime.js'")) 17 | 18 | fs.writeFileSync(originalJsonpMainTemplatePath, overridenJsonpMainTemplate) 19 | 20 | const originalLogApplyResultPath = require.resolve(path.join(__dirname, '../../node_modules/webpack/hot/log-apply-result.js')) 21 | const overridenLogApplyResultPath = require.resolve(path.join(__dirname, './template/log-apply-results.js')) 22 | const overridenLogApplyResult = fs.readFileSync(overridenLogApplyResultPath, {encoding: "utf8"}) 23 | 24 | console.log(clc.green("Overriding 'node_modules/webpack/hot/log-apply-result.js'")) 25 | 26 | fs.writeFileSync(originalLogApplyResultPath, overridenLogApplyResult) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /dev-env/override/template/JsonpMainTemplate.runtime.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License http://www.opensource.org/licenses/mit-license.php 3 | Author Tobias Koppers @sokra 4 | */ 5 | /*globals hotAddUpdateChunk parentHotUpdateCallback document XMLHttpRequest $require$ $hotChunkFilename$ $hotMainFilename$ */ 6 | 7 | module.exports = function() { 8 | function webpackHotUpdateCallback(chunkId, moreModules) { // eslint-disable-line no-unused-vars 9 | hotAddUpdateChunk(chunkId, moreModules); 10 | if(parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules); 11 | } 12 | 13 | //////////////////// START 14 | // Schovi's hotDownloadUpdateChunk overwriter for current context 15 | 16 | console.log(">> Using custom overriden hotDownloadUpdateChunk") 17 | 18 | var context = this; 19 | 20 | // http://stackoverflow.com/questions/8403108/calling-eval-in-particular-context/25859853#25859853 21 | function evalInContext(js, context) { 22 | return function() { return eval(js); }.call(context); 23 | } 24 | 25 | function reqListener () { 26 | evalInContext(this.responseText, context) 27 | } 28 | 29 | context.hotDownloadUpdateChunk = function (chunkId) { // eslint-disable-line no-unused-vars 30 | var src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js"; 31 | var request = new XMLHttpRequest(); 32 | 33 | request.onload = reqListener; 34 | request.open("get", src, true); 35 | request.send(); 36 | } 37 | //////////////////// END 38 | 39 | 40 | function hotDownloadManifest(callback) { // eslint-disable-line no-unused-vars 41 | if(typeof XMLHttpRequest === "undefined") 42 | return callback(new Error("No browser support")); 43 | try { 44 | var request = new XMLHttpRequest(); 45 | var requestPath = $require$.p + $hotMainFilename$; 46 | request.open("GET", requestPath, true); 47 | request.timeout = 10000; 48 | request.send(null); 49 | } catch(err) { 50 | return callback(err); 51 | } 52 | request.onreadystatechange = function() { 53 | if(request.readyState !== 4) return; 54 | if(request.status === 0) { 55 | // timeout 56 | callback(new Error("Manifest request to " + requestPath + " timed out.")); 57 | } else if(request.status === 404) { 58 | // no update available 59 | callback(); 60 | } else if(request.status !== 200 && request.status !== 304) { 61 | // other failure 62 | callback(new Error("Manifest request to " + requestPath + " failed.")); 63 | } else { 64 | // success 65 | try { 66 | var update = JSON.parse(request.responseText); 67 | } catch(e) { 68 | callback(e); 69 | return; 70 | } 71 | callback(null, update); 72 | } 73 | }; 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /dev-env/override/template/log-apply-results.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License http://www.opensource.org/licenses/mit-license.php 3 | Author Tobias Koppers @sokra 4 | */ 5 | module.exports = function(updatedModules, renewedModules) { 6 | var unacceptedModules = updatedModules.filter(function(moduleId) { 7 | return renewedModules && renewedModules.indexOf(moduleId) < 0; 8 | }); 9 | 10 | if(unacceptedModules.length > 0) { 11 | console.warn("[HMR] The following modules couldn't be hot updated: (They would need a full reload!)"); 12 | unacceptedModules.forEach(function(moduleId) { 13 | console.warn("[HMR] - " + moduleId); 14 | }); 15 | 16 | // Schovi's 'module couldn't be hot updated' fixer 17 | // TODO when we are not in background script, wee can only reload page. Should it be auto? 18 | if(chrome && chrome.runtime && chrome.runtime.reload) { 19 | console.warn("[HMR] Processing full extension reload"); 20 | chrome.runtime.reload() 21 | } else { 22 | console.warn("[HMR] Can't proceed full reload. chrome.runtime.reload is not available"); 23 | } 24 | //////////// 25 | } 26 | 27 | if(!renewedModules || renewedModules.length === 0) { 28 | console.log("[HMR] Nothing hot updated."); 29 | } else { 30 | console.log("[HMR] Updated modules:"); 31 | renewedModules.forEach(function(moduleId) { 32 | console.log("[HMR] - " + moduleId); 33 | }); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /dev-env/paths.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export const root = path.normalize(path.join(__dirname, "..")) 4 | 5 | export const packageJson = path.normalize(path.join(root, "package.json")) 6 | 7 | export const src = path.normalize(path.join(root, "src")) 8 | 9 | export const release = path.normalize(path.join(root, "release")) 10 | 11 | export const build = process.env.NODE_ENV == "development" 12 | ? path.normalize(path.join(root, "build")) 13 | : path.normalize(path.join(release, "build")) 14 | 15 | export const manifest = path.normalize(path.join(src, "manifest.json")) 16 | -------------------------------------------------------------------------------- /dev-env/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | export $(cat .env | xargs) 4 | 5 | if [ $# -eq 0 ] 6 | then 7 | echo "Please provide a release type." 8 | exit 1 9 | fi 10 | 11 | release_type=$1 12 | branch=$(git rev-parse --abbrev-ref HEAD) 13 | 14 | if [ "$branch" != "master" ] 15 | then 16 | echo "Will not release from a branch other than master" 17 | exit 1 18 | fi 19 | 20 | echo "Creating a new version..." 21 | npm version $release_type 22 | echo "Pushing new version and master to origin..." 23 | git push origin master 24 | git push --tags 25 | echo "Building extension..." 26 | npm run build 27 | echo "Creating a new release..." 28 | tag=$(git describe) 29 | ./node_modules/publish-release/bin/publish-release --token $GITHUB_TOKEN \ 30 | --owner jessepollak \ 31 | --repo command \ 32 | --tag $tag \ 33 | --name $tag \ 34 | echo "Release published!" 35 | 36 | echo "Adding latest build to Github pages" 37 | 38 | cp "$(pwd)/release/build/content/index.js" /tmp/ 39 | git checkout gh-pages-develop 40 | cp /tmp/index.js ./source/javascripts/ 41 | git add ./source/javascripts/index.js 42 | git commit -m "add release JS for $tag" 43 | rake publish 44 | git checkout master 45 | 46 | echo "Finished!" 47 | -------------------------------------------------------------------------------- /dev-env/util/remove.js: -------------------------------------------------------------------------------- 1 | import nodePath from 'path' 2 | 3 | export function extension(filepath) { 4 | return filepath.split(".").slice(0,-1).join(".") 5 | } 6 | 7 | export function path(filepath) { 8 | const split = filepath.split(nodePath.sep) 9 | 10 | return split[split.length - 1] 11 | } 12 | 13 | export function all(filepath) { 14 | return extension(path(filepath)) 15 | } 16 | 17 | export function file(filepath) { 18 | return filepath.split(nodePath.sep).slice(0,-1).join(nodePath.sep) 19 | } 20 | -------------------------------------------------------------------------------- /dev-env/webpack/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | 5 | module.exports = function(webpackConfig) { 6 | return new Promise((resolve, reject) => { 7 | webpack(webpackConfig, function(fatalError, stats) { 8 | var jsonStats = stats.toJson(); 9 | 10 | // We can save jsonStats to be analyzed with 11 | // http://webpack.github.io/analyse or 12 | // https://github.com/robertknight/webpack-bundle-size-analyzer. 13 | // var fs = require('fs'); 14 | // fs.writeFileSync('./bundle-stats.json', JSON.stringify(jsonStats)); 15 | 16 | var buildError = fatalError || jsonStats.errors[0] || jsonStats.warnings[0]; 17 | 18 | if (buildError) { 19 | reject(buildError) 20 | } else { 21 | resolve(jsonStats) 22 | } 23 | }) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /dev-env/webpack/config.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path" 3 | import { execSync } from "child_process"; 4 | import webpack from 'webpack'; 5 | import _ from 'lodash'; 6 | import * as Remove from '../util/remove' 7 | import * as paths from '../paths' 8 | import * as config from '../config' 9 | import ManifestPlugin from '../manifest/plugin' 10 | 11 | // NOTE Style preprocessors 12 | // If you want to use any of style preprocessor, add related npm package + loader and uncomment following line 13 | var styleLoaders = { 14 | 'css': '', 15 | // 'less': '!less-loader', 16 | 'scss|sass': '!sass-loader', 17 | // 'styl': '!stylus-loader' 18 | }; 19 | 20 | function makeStyleLoaders() { 21 | return Object.keys(styleLoaders).map(function(ext) { 22 | var prefix = 'css-loader?modules&sourceMap' 23 | var extLoaders = prefix + styleLoaders[ext]; 24 | var loader = 'style-loader!' + extLoaders; 25 | 26 | return { 27 | loader: loader, 28 | test: new RegExp('\\.(' + ext + ')$'), 29 | exclude: /node_modules/ 30 | }; 31 | }); 32 | } 33 | 34 | function configGenerator(Manifest) { 35 | 36 | var isDevelopment = process.env.NODE_ENV != "production" 37 | 38 | return { 39 | ///// Lowlevel config 40 | cache: isDevelopment, 41 | debug: isDevelopment, 42 | devtool: isDevelopment ? 'cheap-module-eval-source-map' : '', 43 | context: __dirname, 44 | node: {__dirname: true}, 45 | 46 | ///// App config 47 | 48 | // Entry points in your app 49 | // There we use scripts from your manifest.json 50 | entry: {}, 51 | 52 | // Output 53 | output: (function() { 54 | var output = { 55 | path: paths.build, 56 | filename: '[name].js' 57 | } 58 | 59 | if(isDevelopment) { 60 | output.chunkFilename = '[name]-[chunkhash].js' 61 | output.publicPath = config.DEV_URL + '/' 62 | } 63 | 64 | return output 65 | })(), 66 | 67 | // Plugins 68 | plugins: (function() { 69 | let plugins = [ 70 | new webpack.optimize.OccurenceOrderPlugin(), 71 | new ManifestPlugin(Manifest), 72 | new webpack.DefinePlugin({ 73 | "global.GENTLY": false, 74 | "process.env": { 75 | NODE_ENV: JSON.stringify(isDevelopment ? 'development' : 'production'), 76 | IS_BROWSER: true, 77 | ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS 78 | } 79 | }) 80 | ]; 81 | 82 | if(isDevelopment) { 83 | // Development plugins for hot reload 84 | plugins = plugins.concat([ 85 | // NotifyPlugin, 86 | new webpack.HotModuleReplacementPlugin(), 87 | // Tell reloader to not reload if there is an error. 88 | new webpack.NoErrorsPlugin() 89 | ]) 90 | } else { 91 | // Production plugins for optimizing code 92 | plugins = plugins.concat([ 93 | new webpack.optimize.UglifyJsPlugin({ 94 | compress: { 95 | // Because uglify reports so many irrelevant warnings. 96 | warnings: false 97 | } 98 | }), 99 | new webpack.optimize.DedupePlugin(), 100 | // new webpack.optimize.LimitChunkCountPlugin({maxChunks: 15}), 101 | // new webpack.optimize.MinChunkSizePlugin({minChunkSize: 10000}), 102 | function() { 103 | this.plugin("done", function(stats) { 104 | if (stats.compilation.errors && stats.compilation.errors.length) { 105 | console.log(stats.compilation.errors) 106 | process.exit(1) 107 | } 108 | }) 109 | } 110 | ]) 111 | } 112 | 113 | // NOTE Custom plugins 114 | // if you need to exclude anything pro loading 115 | // plugins.push(new webpack.IgnorePlugin(/^(vertx|somethingelse)$/)) 116 | 117 | return plugins; 118 | })(), 119 | 120 | // NOTE Override external requires 121 | // If you need to change value of required (imported) module 122 | // for example if you dont want any module import 'net' for various reason like code only for non browser envirinment 123 | externals: { 124 | // net: function() {} 125 | }, 126 | 127 | resolve: { 128 | extensions: [ 129 | '', 130 | '.js', 131 | '.jsx', 132 | '.json' 133 | ], 134 | 135 | // NOTE where webpack resolve modules 136 | modulesDirectories: [ 137 | 'src', 138 | 'node_modules' 139 | ], 140 | 141 | root: [ 142 | path.join(__dirname, "../src") 143 | ], 144 | 145 | // NOTE Aliasing 146 | // If you want to override some path with another. Good for importing same version of React across different libraries 147 | alias: { 148 | // "react$": require.resolve(path.join(__dirname, '../../node_modules/react')) 149 | } 150 | }, 151 | 152 | // Loaders 153 | module: { 154 | loaders: (function() { 155 | var loaders = [] 156 | 157 | // Assets 158 | 159 | // Inline all assets with base64 into javascripts 160 | // TODO make and test requiring assets with url 161 | loaders = loaders.concat([ 162 | { 163 | test: /\.(png|jpg|jpeg|gif|svg)/, 164 | loader: "url-loader?limit=1000000&name=[name]-[hash].[ext]", 165 | exclude: /node_modules/ 166 | }, 167 | { 168 | test: /\.(woff|woff2)/, 169 | loader: "url-loader?limit=1000000&name=[name]-[hash].[ext]", 170 | exclude: /node_modules/ 171 | }, 172 | { 173 | test: /\.(ttf|eot)/, 174 | loader: "url-loader?limit=1000000?name=[name]-[hash].[ext]", 175 | exclude: /node_modules/ 176 | } 177 | ]) 178 | 179 | // Styles 180 | loaders = loaders.concat(makeStyleLoaders()) 181 | loaders = loaders.concat({ 182 | test: /react-spinner\.css$/, 183 | loader: 'style-loader!css-loader' 184 | }) 185 | 186 | // Scripts 187 | loaders = loaders.concat([ 188 | { 189 | test: /\.jsx?$/, 190 | exclude: /node_modules/, 191 | loader: "babel-loader" 192 | } 193 | ]) 194 | 195 | loaders = loaders.concat([ 196 | { 197 | test: /jquery\.rangyinputs\.js$/, 198 | loader: 'imports?jQuery=jquery,$=jquery,this=>window' 199 | } 200 | ]) 201 | 202 | loaders = loaders.concat([ 203 | { 204 | test: /google\-analytics\-bundle\.js$/, 205 | loader: 'imports?this=>window' 206 | } 207 | ]) 208 | 209 | // Json 210 | loaders = loaders.concat([ 211 | { 212 | test: /\.json/, 213 | loader: "json-loader" 214 | } 215 | ]) 216 | 217 | // NOTE Custom loaders 218 | // loaders = loaders.concat([...]) 219 | 220 | return loaders 221 | })() 222 | } 223 | } 224 | } 225 | 226 | module.exports = configGenerator 227 | -------------------------------------------------------------------------------- /dev-env/webpack/server.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var express = require('express') 3 | var webpack = require('webpack'); 4 | var WebpackDevServer = require('webpack-dev-server') 5 | var config = require('../config') 6 | 7 | module.exports = function(webpackConfig) { 8 | var host = "0.0.0.0" 9 | 10 | var compiler = webpack(webpackConfig); 11 | 12 | var webpackDevServerOptions = { 13 | contentBase: config.DEV_URL, 14 | publicPath: webpackConfig.output.publicPath, 15 | https: true, 16 | // lazy: true, 17 | // watchDelay: 50, 18 | hot: true, 19 | // Unfortunately quiet swallows everything even error so it can't be used. 20 | quiet: false, 21 | // No info filters only initial compilation it seems. 22 | noInfo: false, 23 | // noInfo: true, 24 | // Remove console.log mess during watch. 25 | stats: { 26 | // assets: false, 27 | colors: true, 28 | // version: false, 29 | // hash: false, 30 | // timings: false, 31 | // chunks: false, 32 | // chunkModules: false 33 | } 34 | } 35 | 36 | new WebpackDevServer( 37 | compiler, 38 | webpackDevServerOptions 39 | ).listen(config.PORT, host, function (err, result) { 40 | if (err) { 41 | console.log(err) 42 | } else { 43 | console.log('Listening at https://' + host + ':' + config.PORT); 44 | } 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Command", 3 | "version": "1.2.0", 4 | "description": "Making the web better with Slack-like slash commands.", 5 | "private": true, 6 | "scripts": { 7 | "start": "npm run dev", 8 | "dev": "NODE_ENV=development ./node_modules/.bin/babel-node ./dev-env/dev.js", 9 | "build": "NODE_ENV=production ./node_modules/.bin/babel-node ./dev-env/build.js", 10 | "release": "NODE_ENV=production ./dev-env/release" 11 | }, 12 | "keywords": [], 13 | "jshintConfig": { 14 | "asi": true, 15 | "esnext": true 16 | }, 17 | "dependencies": { 18 | "caret-to-end": "^1.0.0", 19 | "classnames": "^2.2.3", 20 | "emojilib": "^2.0.2", 21 | "jquery": "^2.2.0", 22 | "lodash": "^4.3.0", 23 | "lodash.indexby": "^3.1.1", 24 | "lodash.pluck": "^3.1.2", 25 | "rangy": "^1.3.0", 26 | "react": "^0.14.7", 27 | "react-dom": "^0.14.7", 28 | "react-draggable": "^2.0.0-beta3", 29 | "react-native-listener": "^1.0.1", 30 | "react-spinner": "^0.2.3", 31 | "react-webcam": "0.0.10", 32 | "redux": "^3.3.1" 33 | }, 34 | "devDependencies": { 35 | "archiver": "^0.21.0", 36 | "babel-cli": "6.4.5", 37 | "babel-core": "6.4.5", 38 | "babel-loader": "^6.2.2", 39 | "babel-preset-es2015": "6.3.13", 40 | "babel-preset-react": "6.3.13", 41 | "babel-preset-react-hmre": "1.0.1", 42 | "chokidar": "1.4.2", 43 | "cli-color": "1.1.0", 44 | "css-loader": "0.23.1", 45 | "dotenv": "^2.0.0", 46 | "express": "4.13.4", 47 | "file-loader": "0.8.5", 48 | "fs-extra": "0.26.5", 49 | "git-rev": "^0.2.1", 50 | "imports-loader": "^0.6.5", 51 | "jshint-jsx": "^0.4.1", 52 | "json-loader": "0.5.4", 53 | "node-sass": "^3.4.2", 54 | "publish-release": "^1.2.0", 55 | "sass-loader": "^3.1.2", 56 | "shelljs": "^0.6.0", 57 | "style-loader": "0.13.0", 58 | "url-loader": "0.5.7", 59 | "webpack": "^1.12.13", 60 | "webpack-dev-server": "^1.14.1", 61 | "yargs": "^4.2.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /resources/command.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jessepollak/command/e0f7b027f63ea8bcfe1ecc2e3f342aebeec089f6/resources/command.gif -------------------------------------------------------------------------------- /src/background/analytics.js: -------------------------------------------------------------------------------- 1 | import './vendor/google-analytics-bundle' 2 | 3 | var tracker 4 | 5 | function shouldSend() { 6 | return (process.env.NODE_ENV == 'production' || process.env.ENABLE_ANALYTICS) 7 | } 8 | 9 | function addListener() { 10 | chrome.runtime.onMessage.addListener( 11 | (request, sender, sendResponse) => { 12 | if (request.method == 'sendEvent') { 13 | sendEvent(request.category, request.action, request.label, request.value) 14 | } 15 | }); 16 | } 17 | 18 | function configureGA() { 19 | let service = analytics.getService('command') 20 | tracker = service.getTracker('UA-75050322-1') 21 | sendEvent('initialize') 22 | sendView('Background Page') 23 | } 24 | 25 | function sendView(view) { 26 | if (!shouldSend()) return 27 | tracker.sendAppView(view) 28 | } 29 | 30 | function sendEvent(category, action, label, value) { 31 | if (!shouldSend()) return 32 | tracker.sendEvent(category, action, label, value) 33 | } 34 | 35 | export function setup() { 36 | if (!shouldSend()) return 37 | 38 | configureGA() 39 | addListener() 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/background/headers.js: -------------------------------------------------------------------------------- 1 | export function setup() { 2 | // Listens when new request 3 | chrome.webRequest.onHeadersReceived.addListener(function(details) { 4 | for (let i = 0; i < details.responseHeaders.length; i++) { 5 | 6 | if (isCSPHeader(details.responseHeaders[i].name.toUpperCase())) { 7 | var csp = details.responseHeaders[i].value; 8 | csp = csp.replace('media-src', "media-src blob:"); 9 | details.responseHeaders[i].value = csp; 10 | } 11 | } 12 | 13 | return { // Return the new HTTP header 14 | responseHeaders: details.responseHeaders 15 | }; 16 | }, { 17 | urls: ["https://github.com/*"], 18 | types: ["main_frame"] 19 | }, ["blocking", "responseHeaders"]); 20 | } 21 | 22 | function isCSPHeader(headerName) { 23 | return (headerName == 'CONTENT-SECURITY-POLICY') || (headerName == 'X-WEBKIT-CSP'); 24 | } 25 | -------------------------------------------------------------------------------- /src/background/index.js: -------------------------------------------------------------------------------- 1 | import * as Analytics from './analytics' 2 | import * as Headers from './headers' 3 | 4 | Analytics.setup() 5 | Headers.setup() 6 | -------------------------------------------------------------------------------- /src/content/commands/Emoji/Emoji.jsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import $ from 'jquery' 3 | import React from 'react' 4 | import classnames from 'classnames' 5 | import { mountReactComponent } from 'content/commands/mount' 6 | import * as Emojilib from 'emojilib' 7 | 8 | import 'react-spinner/react-spinner.css' 9 | import styles from './Emoji.scss' 10 | import * as Types from 'content/types' 11 | import * as Search from 'content/components/Search' 12 | import Container from 'content/components/Container' 13 | 14 | // set IDs for keys 15 | _.map(Emojilib.lib, (v, k) => { 16 | v.id = k 17 | return v 18 | }) 19 | 20 | let doesSupportEmoji = () => { 21 | if (!document.createElement('canvas').getContext) return 22 | var context = document.createElement('canvas').getContext('2d') 23 | if (typeof context.fillText != 'function') return 24 | // :smile: String.fromCharCode(55357) + String.fromCharCode(56835) 25 | let smile = String.fromCodePoint(0x1F604) 26 | 27 | context.textBaseline = "top" 28 | context.font = "32px Arial" 29 | context.fillText(smile, 0, 0) 30 | return context.getImageData(16, 16, 1, 1).data[0] !== 0 31 | } 32 | 33 | let EMOJIS_BY_CATEGORY = _.reduce(Emojilib.lib, (memo, v, k) => { 34 | let category = v.category 35 | if (!memo[category]) { 36 | memo[category] = [] 37 | } 38 | memo[category].push(v) 39 | return memo 40 | }, {}) 41 | 42 | let EmojiItem = (props) => { 43 | return ( 44 |Your browser does not support emoji.
182 | } 183 | 184 | return ( 185 |Pro tip: shift-click to insert more than one emoji.
187 |{props.name}
32 |{props.artists[0].name}
33 |No results.
183 | ) 184 | } 185 | 186 | export class Widget extends React.Component { 187 | constructor(props) { 188 | super(props) 189 | 190 | this.state = { 191 | results: props.results || [], 192 | IS_LOADING: false, 193 | query: "", 194 | page: 0 195 | } 196 | 197 | this.search = this.search.bind(this) 198 | this.onSelect = this.onSelect.bind(this) 199 | this.onBack = this.onBack.bind(this) 200 | this.onForward = this.onForward.bind(this) 201 | this.onReachEnd = this.onReachEnd.bind(this) 202 | } 203 | 204 | componentWillUnmount() { 205 | if (this.pendingRequest) { 206 | this.cancel = true 207 | } 208 | } 209 | 210 | onBack() { 211 | this.setState({ page: Math.max(0, this.state.page - 1) }) 212 | } 213 | 214 | onForward() { 215 | this.setState({ page: this.state.page + 1 }) 216 | } 217 | 218 | onReachEnd() { 219 | this.doQuery(this.state.query, { append: true, offset: this.state.results.length }) 220 | } 221 | 222 | doQuery(query, options={}) { 223 | this.pendingRequest = this.props.onSearch(query, options) 224 | .then((results) => { 225 | if (this.cancel) return 226 | results = options.append ? this.state.results.concat(results) : results 227 | this.setState({ results: results, IS_LOADING: false }) 228 | }) 229 | .always(() => { 230 | this.pendingRequest = null 231 | }) 232 | } 233 | 234 | search(query) { 235 | if (query == "") { 236 | return this.setState({ 237 | query: query, 238 | IS_LOADING: false, 239 | results: [], 240 | page: 0 241 | }) 242 | } 243 | 244 | this.setState({ query: query, IS_LOADING: true, results: [] }) 245 | this.doQuery(query) 246 | } 247 | 248 | onSelect(result) { 249 | this.props.onSelect(result) 250 | } 251 | 252 | render() { 253 | let classes = classnames(styles.widget, this.props.className, { 254 | [styles.isExpanded]: this.state.query != "" || this.props.isExpanded, 255 | [styles.hasResults]: this.state.results.length > 0, 256 | [styles.hasColumns]: this.props.columns 257 | }) 258 | 259 | let toRender 260 | if (this.state.IS_LOADING) { 261 | toRender =