├── .eslintignore ├── .gitignore ├── .prettierignore ├── images ├── marquee.png ├── large-tile.png ├── screenshot-1.png ├── small-tile.png ├── logo-transparent.png └── screenshot-v1.0.0.png ├── static └── icons │ ├── icon_128.png │ ├── icon_16.png │ └── icon_48.png ├── docs ├── roadmap.md ├── publishing.md ├── faq.md ├── schema.md └── async-store-workaround.md ├── src ├── mode-javascript-eslint │ ├── package.json │ ├── mode-javascript-eslint.js │ ├── mirror.js │ ├── worker-javascript-eslint.js │ ├── worker.js │ └── package-lock.json ├── devtools.ts ├── declarations.d.ts ├── types.ts ├── editor │ ├── reducers │ │ ├── index.ts │ │ ├── settings.ts │ │ └── snippets.ts │ ├── middleware │ │ ├── log-error.ts │ │ ├── settings.ts │ │ └── save-when-inactive.ts │ ├── components │ │ ├── Loading.tsx │ │ ├── SettingsGroup.tsx │ │ ├── Welcome.tsx │ │ ├── SnippetList.tsx │ │ ├── Sidepane.tsx │ │ ├── ErrorPage.tsx │ │ ├── Login.tsx │ │ ├── Editor.tsx │ │ ├── SnippetSelector.tsx │ │ ├── SelectGist.tsx │ │ ├── App.tsx │ │ ├── Main.tsx │ │ └── Settings.tsx │ ├── actions │ │ ├── settings.ts │ │ └── snippets.ts │ ├── util │ │ ├── deep-merge.ts │ │ └── generate-redux.ts │ ├── constants.ts │ ├── settings.css │ ├── main.css │ ├── index.tsx │ └── aliases.ts ├── manifest.json ├── panel.html ├── panel.ts ├── background.ts ├── lib │ └── timer.ts └── test.ts ├── .babelrc ├── tsconfig.json ├── README.md ├── package.json ├── CHANGELOG.md ├── webpack.config.js └── .eslintrc.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | npm-debug.log 4 | build 5 | .vscode 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | npm-debug.log 4 | build 5 | .vscode 6 | -------------------------------------------------------------------------------- /images/marquee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SidneyNemzer/snippets/HEAD/images/marquee.png -------------------------------------------------------------------------------- /images/large-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SidneyNemzer/snippets/HEAD/images/large-tile.png -------------------------------------------------------------------------------- /images/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SidneyNemzer/snippets/HEAD/images/screenshot-1.png -------------------------------------------------------------------------------- /images/small-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SidneyNemzer/snippets/HEAD/images/small-tile.png -------------------------------------------------------------------------------- /static/icons/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SidneyNemzer/snippets/HEAD/static/icons/icon_128.png -------------------------------------------------------------------------------- /static/icons/icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SidneyNemzer/snippets/HEAD/static/icons/icon_16.png -------------------------------------------------------------------------------- /static/icons/icon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SidneyNemzer/snippets/HEAD/static/icons/icon_48.png -------------------------------------------------------------------------------- /images/logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SidneyNemzer/snippets/HEAD/images/logo-transparent.png -------------------------------------------------------------------------------- /images/screenshot-v1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SidneyNemzer/snippets/HEAD/images/screenshot-v1.0.0.png -------------------------------------------------------------------------------- /docs/roadmap.md: -------------------------------------------------------------------------------- 1 | See the [Github Project](https://github.com/SidneyNemzer/snippets/projects/1) for planned features 2 | -------------------------------------------------------------------------------- /src/mode-javascript-eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "eslint": "latest" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/devtools.ts: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create( 2 | "Snippets", 3 | "", // We don't provide an image 4 | "../panel.html" 5 | ); 6 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const src: string; 3 | export default src; 4 | } 5 | 6 | declare const ace: any; 7 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type DeepPartial = { 2 | [P in keyof T]?: T[P] extends (infer U)[] 3 | ? DeepPartial[] 4 | : T[P] extends Readonly[] 5 | ? Readonly>[] 6 | : DeepPartial; 7 | }; 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ], 7 | "plugins": [ 8 | "@babel/plugin-proposal-class-properties", 9 | "@babel/plugin-proposal-optional-chaining" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /docs/publishing.md: -------------------------------------------------------------------------------- 1 | 1. Make changes to the code 2 | 2. Change the version in `package.json` 3 | 3. Update `CHANGELOG.md` 4 | 4. Commit changes 5 | 5. `npm run build:prod` 6 | 6. Create a zip file of `build/` 7 | 7. Upload to the Chrome Web Store and publish 8 | 8. Create a tag 9 | 9. Clear Done column in Github Project 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "allowJs": false, 5 | "noEmit": true, 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "preserve" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/editor/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | import { reducer as settings, SettingsState } from "./settings"; 4 | import snippets, { SnippetsState } from "./snippets"; 5 | 6 | export type RootState = { 7 | snippets: SnippetsState; 8 | settings: SettingsState; 9 | }; 10 | 11 | export default combineReducers({ 12 | settings, 13 | snippets, 14 | }); 15 | -------------------------------------------------------------------------------- /src/editor/middleware/log-error.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "redux"; 2 | 3 | const logError: Middleware = (store) => (next) => (action) => { 4 | if (action.error) { 5 | if (action.error.context) { 6 | console.error("Failed to " + action.error.context, action.error); 7 | } else { 8 | console.error(action.error); 9 | } 10 | } 11 | next(action); 12 | }; 13 | 14 | export default logError; 15 | -------------------------------------------------------------------------------- /src/editor/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import CircularProgress from "@material-ui/core/CircularProgress"; 2 | import React from "react"; 3 | 4 | const Loading: React.FC = () => ( 5 |
13 |

Loading

14 | 15 |
16 | ); 17 | 18 | export default Loading; 19 | -------------------------------------------------------------------------------- /src/editor/components/SettingsGroup.tsx: -------------------------------------------------------------------------------- 1 | import Paper from "@material-ui/core/Paper"; 2 | import React from "react"; 3 | 4 | type Props = { 5 | className?: string; 6 | label: string; 7 | }; 8 | 9 | const SettingsGroup: React.FC = ({ 10 | className = "", 11 | label, 12 | children, 13 | }) => ( 14 |
15 |

{label}

16 | {children} 17 |
18 | ); 19 | 20 | export default SettingsGroup; 21 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Snippets", 3 | "description": "Create and edit JavaScript code snippets, which are synced to all your computers", 4 | "manifest_version": 3, 5 | 6 | "background": { 7 | "service_worker": "background.js" 8 | }, 9 | "devtools_page": "devtools.html", 10 | 11 | "permissions": ["storage", "alarms"], 12 | 13 | "icons": { 14 | "16": "icons/icon_16.png", 15 | "48": "icons/icon_48.png", 16 | "128": "icons/icon_128.png" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Snippets 7 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/editor/actions/settings.ts: -------------------------------------------------------------------------------- 1 | import { generateTypes, generateActions } from "../util/generate-redux"; 2 | 3 | const typeNames = { 4 | tabSize: true, 5 | autoComplete: true, 6 | softTabs: true, 7 | theme: true, 8 | lineWrap: true, 9 | linter: true, 10 | accessToken: true, 11 | gistId: true, 12 | autosaveTimer: true, 13 | fontSize: true, 14 | } as const; 15 | 16 | export type Types = keyof typeof typeNames; 17 | 18 | export const types = generateTypes(typeNames); 19 | export const actions = generateActions(typeNames); 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Snippets](images/logo-transparent.png)][chrome-web-store] 2 | 3 | [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/fakjeijchchmicjllnabpdkclfkpbiag.svg)][chrome-web-store] 4 | 5 | A Chrome extension that allows you to create and edit JavaScript code snippets, which are synced to all your computers. After installing the extension, use the editor by **opening the devtools** and selecting the **snippets tab**. 6 | 7 | [**INSTALL**][chrome-web-store] | [**FAQ**](docs/faq.md) | [**CHANGELOG**](CHANGELOG.md) | [**ROADMAP**][project] 8 | 9 | [chrome-web-store]: https://chrome.google.com/webstore/detail/snippets/fakjeijchchmicjllnabpdkclfkpbiag 10 | [project]: https://github.com/SidneyNemzer/snippets/projects/1 11 | -------------------------------------------------------------------------------- /src/editor/util/deep-merge.ts: -------------------------------------------------------------------------------- 1 | /* Source: 2 | https://stackoverflow.com/a/34749873/7486612 3 | */ 4 | 5 | export const isObject = (item: unknown): boolean => 6 | !!item && typeof item === "object" && !Array.isArray(item); 7 | 8 | export const mergeDeep = ( 9 | target: T, 10 | ...sources: T[] 11 | ): T => { 12 | if (!sources.length) return target; 13 | const source = sources.shift(); 14 | 15 | if (isObject(target) && isObject(source)) { 16 | for (const key in source) { 17 | if (isObject(source[key])) { 18 | if (!(target as any)[key]) Object.assign(target, { [key]: {} }); 19 | mergeDeep((target as any)[key], source[key]); 20 | } else { 21 | Object.assign(target, { [key]: source[key] }); 22 | } 23 | } 24 | } 25 | 26 | return mergeDeep(target, ...sources); 27 | }; 28 | -------------------------------------------------------------------------------- /src/editor/reducers/settings.ts: -------------------------------------------------------------------------------- 1 | import { types } from "../actions/settings"; 2 | import { generateReducer } from "../util/generate-redux"; 3 | 4 | export type SettingsState = { 5 | tabSize: number; 6 | autoComplete: boolean; 7 | softTabs: boolean; 8 | theme: string; 9 | lineWrap: boolean; 10 | linter: boolean; 11 | accessToken: string | false; 12 | gistId: string | false; 13 | autosaveTimer: number; 14 | fontSize: number; 15 | }; 16 | 17 | export const defaultState: SettingsState = { 18 | [types.tabSize]: 2, 19 | [types.autoComplete]: true, 20 | [types.softTabs]: true, 21 | [types.theme]: "github", 22 | [types.lineWrap]: false, 23 | [types.linter]: true, 24 | [types.accessToken]: false, 25 | [types.gistId]: false, 26 | [types.autosaveTimer]: 5, 27 | [types.fontSize]: 12, 28 | }; 29 | 30 | export const reducer = generateReducer(defaultState); 31 | -------------------------------------------------------------------------------- /src/panel.ts: -------------------------------------------------------------------------------- 1 | import createEditor from "./editor"; 2 | 3 | const logInInspected = (level: string, message: unknown) => { 4 | chrome.devtools.inspectedWindow.eval( 5 | `console.${level}(${JSON.stringify(message)})` 6 | ); 7 | }; 8 | 9 | const evalInInspected = (content: string) => { 10 | chrome.devtools.inspectedWindow.eval( 11 | content, 12 | {}, 13 | ( 14 | result: unknown, 15 | // exceptionInfo is undefined if an exception was not thrown, but 16 | // @types/chrome does not reflect this. 17 | exceptionInfo: 18 | | chrome.devtools.inspectedWindow.EvaluationExceptionInfo 19 | | undefined 20 | ) => { 21 | if (exceptionInfo?.isException && exceptionInfo.value) { 22 | logInInspected("error", exceptionInfo.value); 23 | } 24 | } 25 | ); 26 | }; 27 | 28 | createEditor(evalInInspected, String(chrome.devtools.inspectedWindow.tabId)); 29 | -------------------------------------------------------------------------------- /src/editor/middleware/settings.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Middleware } from "redux"; 2 | 3 | import { types } from "../actions/settings"; 4 | 5 | // This middleware handles saving settings to the current environment's 6 | // storage. In Chrome, this is chrome.storage.sync. In the browser test page, 7 | // it's localStorage 8 | 9 | type Storage = { 10 | set: (key: string, value: any) => void; 11 | }; 12 | 13 | const isSettingAction = (action: AnyAction) => 14 | Object.keys(types).includes(action.type); 15 | 16 | const settingActionPayload = (action: AnyAction) => 17 | action[types[action.type as keyof typeof types]]; 18 | 19 | const settingsMiddleware = (storage: Storage): Middleware => (store) => ( 20 | next 21 | ) => (action) => { 22 | if (isSettingAction(action)) { 23 | storage.set(action.type, settingActionPayload(action)); 24 | } 25 | next(action); 26 | }; 27 | 28 | export default settingsMiddleware; 29 | -------------------------------------------------------------------------------- /src/editor/constants.ts: -------------------------------------------------------------------------------- 1 | export const pages = { 2 | MAIN: "/main", 3 | SETTINGS: "/settings", 4 | LOGIN: "/login", 5 | SELECT_GIST: "/select-gist", 6 | WELCOME: "/welcome", 7 | }; 8 | 9 | export const CREATE_ACCESS_TOKEN_URL = 10 | "https://github.com/settings/tokens/new?description=Snippets%20Access%20Token&scopes=gist"; 11 | 12 | export const SNIPPETS_ISSUES_URL = 13 | "https://github.com/SidneyNemzer/snippets/issues/new"; 14 | 15 | export const OCTOKIT_USER_AGENT = `github.com/sidneynemzer/snippets ${process.env.SNIPPETS_VERSION}`; 16 | 17 | export const WELCOME_SNIPPET_CONTENT = ` 18 | /*********************** 19 | * Welcome to Snippets! * 20 | ***********************/ 21 | 22 | console.log('Welcome to snippets!') 23 | 24 | /* 25 | CONTROLS 26 | 27 | * Run a snippet in the page that you opened the devtools on 28 | CTRL+ENTER 29 | (You must have the snippet focused) 30 | 31 | * Toggle the devtools console 32 | ESC 33 | 34 | BUGS / ISSUES / SUGGESTIONS 35 | 36 | https://github.com/SidneyNemzer/snippets/issues 37 | 38 | HAPPY CODING! 39 | */ 40 | `; 41 | -------------------------------------------------------------------------------- /src/editor/components/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import Button from "@material-ui/core/Button"; 2 | import React from "react"; 3 | import { Link } from "react-router-dom"; 4 | 5 | import logo from "../../../images/logo-transparent.png"; 6 | import { pages } from "../constants"; 7 | 8 | const Welcome: React.FC = () => ( 9 |
10 | 11 |

Welcome to Snippets!

12 |

13 | Snippets are stored in a Github Gist, so you'll need to authenticate 14 | with Github.
(We'll use a personal access token to do that). 15 |

16 | 17 | 20 | 21 |

22 | If you used Snippets before version 2.0:
23 | you can import snippets from Chrome storage in the settings, after you 24 | login 25 |

26 |
27 | ); 28 | 29 | export default Welcome; 30 | -------------------------------------------------------------------------------- /src/editor/middleware/save-when-inactive.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "redux"; 2 | 3 | import { createTimer } from "../../lib/timer"; 4 | import { 5 | CREATE_SNIPPET, 6 | RENAME_SNIPPET, 7 | UPDATE_SNIPPET, 8 | DELETE_SNIPPET, 9 | SAVING_SNIPPETS, 10 | LOADED_LEGACY_SNIPPETS, 11 | saveSnippets, 12 | } from "../actions/snippets"; 13 | 14 | const modifyActions = [ 15 | CREATE_SNIPPET, 16 | RENAME_SNIPPET, 17 | UPDATE_SNIPPET, 18 | DELETE_SNIPPET, 19 | LOADED_LEGACY_SNIPPETS, 20 | ]; 21 | 22 | const timer = createTimer("save"); 23 | 24 | const saveWhenInactive: Middleware = (store) => { 25 | timer.setCallback(() => store.dispatch(saveSnippets())); 26 | 27 | return (next) => (action) => { 28 | const enabled = store.getState().settings.autosaveTimer > 0; 29 | 30 | if (modifyActions.includes(action.type) && enabled) { 31 | const delayMs = store.getState().settings.autosaveTimer * 1000; 32 | timer.set(delayMs); 33 | } else if (action.type === SAVING_SNIPPETS) { 34 | timer.cancel(); 35 | } 36 | next(action); 37 | }; 38 | }; 39 | 40 | export default saveWhenInactive; 41 | -------------------------------------------------------------------------------- /src/editor/components/SnippetList.tsx: -------------------------------------------------------------------------------- 1 | import List from "@material-ui/core/List"; 2 | import React from "react"; 3 | 4 | import { Snippet } from "../reducers/snippets"; 5 | import SnippetSelector from "./SnippetSelector"; 6 | 7 | type Props = { 8 | snippets: { [name: string]: Snippet }; 9 | selectedSnippet: string | null; 10 | selectSnippet: (name: string) => void; 11 | renameSnippet: (oldName: string, newName: string) => void; 12 | deleteSnippet: (name: string) => void; 13 | runSnippet: (name: string) => void; 14 | }; 15 | 16 | const SnippetList: React.FC = ({ 17 | snippets, 18 | selectedSnippet, 19 | selectSnippet, 20 | renameSnippet, 21 | deleteSnippet, 22 | runSnippet, 23 | }) => ( 24 | 25 | {Object.entries(snippets).map(([name, snippet]) => ( 26 | selectSnippet(name)} 29 | selected={selectedSnippet === name} 30 | updateName={(newName) => renameSnippet(name, newName)} 31 | name={snippet.renamed || name} 32 | deleteSnippet={() => deleteSnippet(name)} 33 | runSnippet={() => runSnippet(snippet.content.local)} 34 | /> 35 | ))} 36 | 37 | ); 38 | 39 | export default SnippetList; 40 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | _If your question isn't answered here, you can [open an issue](https://github.com/SidneyNemzer/snippets/issues/new)_ 4 | 5 | ## How do I use this extension? 6 | 7 | After installing the extension, open the Chrome Devtools on any webpage. This extension adds a new tab called "Snippets", where you can view, edit, and run your JavaScript code snippets. 8 | 9 | ## Chrome has built-in snippets. Why not sync those? 10 | 11 | Unfortunately, Chrome doesn't allow extensions to access the built-in snippets, nor does it sync those on its own. So I created my own snippet editor! 12 | 13 | ## What did you use to make this extension? 14 | 15 | The interface uses [React](https://facebook.github.io/react/) and [Material UI](https://material-ui.com/). The code editor is [react-ace](https://github.com/securingsincity/react-ace). [Redux](https://redux.js.org/) and [webext-redux](https://github.com/tshaddix/webext-redux) are used to sync all panels. 16 | 17 | ## Can you add _[insert feature here]_ 18 | 19 | See [the Github Project](https://github.com/SidneyNemzer/snippets/projects/1) for planned features. [Open an issue](https://github.com/SidneyNemzer/snippets/issues) if you'd like to suggest a feature. 20 | 21 | ## What font is the logo in? 22 | 23 | Freestyle Script. Maybe you don't find that too interesting, but I'll forget it if I don't write it down somewhere... 24 | -------------------------------------------------------------------------------- /src/editor/components/Sidepane.tsx: -------------------------------------------------------------------------------- 1 | import Button from "@material-ui/core/Button"; 2 | import SettingsIcon from "@material-ui/icons/Settings"; 3 | import React from "react"; 4 | 5 | import { Snippet } from "../reducers/snippets"; 6 | import SnippetList from "./SnippetList"; 7 | 8 | type Props = { 9 | snippets: { [name: string]: Snippet }; 10 | selectedSnippet: string | null; 11 | handleOpenSettings: () => void; 12 | createSnippet: (name: string) => void; 13 | selectSnippet: (name: string) => void; 14 | renameSnippet: (oldName: string, newName: string) => void; 15 | deleteSnippet: (name: string) => void; 16 | runSnippet: (name: string) => void; 17 | }; 18 | 19 | const Sidepane: React.FC = (props) => ( 20 |
21 | 29 | 37 | 40 |
41 | ); 42 | 43 | export default Sidepane; 44 | -------------------------------------------------------------------------------- /docs/schema.md: -------------------------------------------------------------------------------- 1 | # Schema 2 | 3 | This file documents how Snippets stores data 4 | 5 | ## Redux Store 6 | 7 | ```javascript 8 | { 9 | snippets: { 10 | loading: false, // Boolean 11 | saving: false, // Boolean 12 | error: null, // String or null 13 | data: { 14 | [name]: { 15 | deleted: false, // Boolean 16 | renamed: 'new-name', // String or false 17 | lastUpdatedBy: 'chrome-devtools://devt...kSide=undocked', // String or undefined 18 | // lastUpdatedBy is used by each panel's editor to know when to update 19 | // the editor in response to a change from a different panel. Editors 20 | // can ignore their own updates. 21 | content: { 22 | local: 'abcd', // String 23 | remote: 'defg' // String or false 24 | } 25 | } 26 | } 27 | 28 | settings: { 29 | tabSize: 2, // Int 30 | autoComplete: true, // Boolean 31 | softTabs: true, // Boolean 32 | theme: 'github', // 'github' or 'tomorrow_night' 33 | lineWrap: false, // Boolean 34 | linter: true, // Boolean 35 | accessToken: 'abcd', // false or String 36 | gistId: 'abcd', // false or String 37 | autosaveTimer: 5, // number 38 | fontSize: 12, // number 39 | } 40 | } 41 | ``` 42 | 43 | ## Github Gist 44 | 45 | ```javascript 46 | { 47 | public: false, 48 | description: 'Snippets ' 49 | files: { 50 | [name]: { 51 | content: 'content' 52 | } 53 | } 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /src/editor/settings.css: -------------------------------------------------------------------------------- 1 | .settings { 2 | background: #eeeeee; 3 | overflow: auto; 4 | padding-bottom: 50px; 5 | } 6 | 7 | .settings .title { 8 | font-size: 1.5em; 9 | font-weight: normal; 10 | } 11 | 12 | .settings header > div { 13 | display: flex; 14 | justify-content: center; 15 | } 16 | .settings header button { 17 | position: absolute; 18 | left: 20px; 19 | } 20 | .settings header button span svg { 21 | color: white; 22 | } 23 | 24 | .settings main { 25 | max-width: 800px; 26 | margin: auto; 27 | } 28 | 29 | .settings .settings-group > h2 { 30 | color: gray; 31 | } 32 | 33 | .settings .about > div { 34 | text-align: center; 35 | padding: 20px; 36 | } 37 | .settings .logo { 38 | height: 150px; 39 | } 40 | .settings .about .version { 41 | margin: 10px 0 0; 42 | color: gray; 43 | } 44 | .settings .about .author { 45 | margin: 10px 0 0; 46 | } 47 | .settings .about a { 48 | text-decoration: none; 49 | } 50 | .settings .repo { 51 | margin-top: 20px; 52 | } 53 | 54 | .settings .gist-id-input { 55 | width: 286px; 56 | } 57 | 58 | .settings .settings-input { 59 | margin-right: 15px; 60 | } 61 | 62 | .settings .small-number-input { 63 | width: 50px; 64 | } 65 | 66 | .settings .small-number-input input { 67 | text-align: center; 68 | } 69 | 70 | .settings .toggle-access-token-button { 71 | padding: 5px; 72 | margin: 0 8px; 73 | } 74 | 75 | .settings .access-token-input-large { 76 | width: 354px; 77 | } 78 | 79 | .settings .access-token-input-small { 80 | width: 218px; 81 | } 82 | -------------------------------------------------------------------------------- /src/mode-javascript-eslint/mode-javascript-eslint.js: -------------------------------------------------------------------------------- 1 | /* global ace */ 2 | 3 | ace.define("ace/mode/javascript-eslint", function (require, exports, module) { 4 | const oop = require("ace/lib/oop"); 5 | const WorkerClient = require("ace/worker/worker_client").WorkerClient; 6 | const JavaScriptMode = require("ace/mode/javascript").Mode; 7 | const config = require("ace/config"); 8 | 9 | // loadWorkerFromBlob is true by default. It's disabled here because loading 10 | // from a blob is blocked by Chrome's CSP. The CSP must have changed since the 11 | // eslint mode was introduced (it used to work fine). 12 | config.set("loadWorkerFromBlob", false); 13 | 14 | const Mode = function () { 15 | JavaScriptMode.call(this); 16 | }; 17 | oop.inherits(Mode, JavaScriptMode); 18 | 19 | (function () { 20 | this.createWorker = function (session) { 21 | var worker = new WorkerClient( 22 | ["ace"], 23 | "ace/mode/javascript_worker_eslint", 24 | "JavaScriptWorkerEslint" 25 | ); 26 | worker.attachToDocument(session.getDocument()); 27 | 28 | worker.on("annotate", function (results) { 29 | session.setAnnotations(results.data); 30 | }); 31 | 32 | worker.on("terminate", function () { 33 | session.clearAnnotations(); 34 | }); 35 | 36 | return worker; 37 | }; 38 | }.call(Mode.prototype)); 39 | 40 | exports.Mode = Mode; 41 | }); 42 | 43 | ace.config.setModuleUrl( 44 | "ace/mode/javascript_worker_eslint", 45 | "/worker-javascript-eslint.js" 46 | ); 47 | -------------------------------------------------------------------------------- /src/editor/actions/snippets.ts: -------------------------------------------------------------------------------- 1 | export const CREATE_SNIPPET = "CREATE_SNIPPET"; 2 | export const RENAME_SNIPPET = "RENAME_SNIPPET"; 3 | export const UPDATE_SNIPPET = "UPDATE_SNIPPET"; 4 | export const DELETE_SNIPPET = "DELETE_SNIPPET"; 5 | export const LOADING_SNIPPETS = "LOADING_SNIPPETS"; 6 | export const LOADED_SNIPPETS = "LOADED_SNIPPETS"; 7 | export const SAVING_SNIPPETS = "SAVING_SNIPPETS"; 8 | export const SAVED_SNIPPETS = "SAVED_SNIPPETS"; 9 | export const LOAD_SNIPPETS = "LOAD_SNIPPETS"; 10 | export const SAVE_SNIPPETS = "SAVE_SNIPPETS"; 11 | export const LOAD_LEGACY_SNIPPETS = "LOAD_LEGACY_SNIPPETS"; 12 | export const LOADED_LEGACY_SNIPPETS = "LOADED_LEGACY_SNIPPETS"; 13 | 14 | export const createSnippet = () => ({ 15 | type: CREATE_SNIPPET, 16 | }); 17 | 18 | export const renameSnippet = (oldName: string, newName: string) => ({ 19 | type: RENAME_SNIPPET, 20 | oldName, 21 | newName, 22 | }); 23 | 24 | export const updateSnippet = ( 25 | name: string, 26 | newBody: string, 27 | editorId: string 28 | ) => ({ 29 | type: UPDATE_SNIPPET, 30 | name, 31 | newBody, 32 | editorId, 33 | }); 34 | 35 | export const deleteSnippet = (name: string) => ({ 36 | type: DELETE_SNIPPET, 37 | name, 38 | }); 39 | 40 | export const loadSnippets = () => ({ 41 | type: LOAD_SNIPPETS, 42 | }); 43 | 44 | export const loadLegacySnippets = () => ({ 45 | type: LOAD_LEGACY_SNIPPETS, 46 | }); 47 | 48 | export type Snippet = { 49 | name: string; 50 | body: string; 51 | }; 52 | 53 | export const loadedSnippets = ( 54 | error: Error | null, 55 | snippets?: { [name: string]: Snippet } 56 | ) => ({ 57 | type: LOADED_SNIPPETS, 58 | snippets, 59 | error, 60 | }); 61 | 62 | export const saveSnippets = () => ({ 63 | type: SAVE_SNIPPETS, 64 | }); 65 | 66 | export const savedSnippets = (error: Error | null) => ({ 67 | type: SAVED_SNIPPETS, 68 | error, 69 | }); 70 | -------------------------------------------------------------------------------- /src/editor/components/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import Button from "@material-ui/core/Button"; 2 | import React from "react"; 3 | 4 | const renderTitle = ( 5 | context: string | undefined, 6 | title: string | undefined 7 | ) => { 8 | if (context) { 9 | return

Failed to {context}

; 10 | } else if (title) { 11 | return

{title}

; 12 | } else { 13 | return

Error

; 14 | } 15 | }; 16 | 17 | const renderError = (error: string | Error | undefined) => { 18 | if (!error) { 19 | return false; 20 | } 21 | 22 | if (typeof error === "string") { 23 | return
{error}
; 24 | } 25 | 26 | if (error.message) { 27 | return
{error.message}
; 28 | } 29 | 30 | return error.toString(); 31 | }; 32 | 33 | const renderAction = ( 34 | action: string | undefined, 35 | actionButton: string | undefined, 36 | onClick: (() => void) | undefined 37 | ) => { 38 | if (action) { 39 | return

{action}

; 40 | } else if (actionButton) { 41 | return ( 42 | 45 | ); 46 | } 47 | }; 48 | 49 | const renderLink = (link: string | undefined) => 50 | link && {link}; 51 | 52 | type Props = { 53 | context?: string; 54 | title?: string; 55 | error?: string | Error; 56 | link?: string; 57 | message?: string; 58 | action?: string; 59 | actionButton?: string; 60 | onActionButtonClick?: () => void; 61 | }; 62 | 63 | const ErrorPage: React.FC = (props) => ( 64 |
65 | {renderTitle(props.context, props.title)} 66 | {props.message &&

{props.message}

} 67 | {renderError(props.error)} 68 | {renderAction(props.action, props.actionButton, props.onActionButtonClick)} 69 | {renderLink(props.link)} 70 |
71 | ); 72 | 73 | export default ErrorPage; 74 | -------------------------------------------------------------------------------- /src/mode-javascript-eslint/mirror.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // This doesn't exist as a stand-alone module in ace-builds, copied from 4 | // https://github.com/ajaxorg/ace/blob/3ebb0c34dc023aff3b6a951dd116a702510369fe/lib/ace/worker/mirror.js 5 | 6 | ace.define("ace/worker/mirror", function (require, exports, module) { 7 | "use strict"; 8 | var Document = require("../document").Document; 9 | var lang = require("../lib/lang"); 10 | 11 | var Mirror = (exports.Mirror = function (sender) { 12 | this.sender = sender; 13 | var doc = (this.doc = new Document("")); 14 | 15 | var deferredUpdate = (this.deferredUpdate = lang.delayedCall( 16 | this.onUpdate.bind(this) 17 | )); 18 | 19 | var _self = this; 20 | sender.on("change", function (e) { 21 | var data = e.data; 22 | if (data[0].start) { 23 | doc.applyDeltas(data); 24 | } else { 25 | for (var i = 0; i < data.length; i += 2) { 26 | if (Array.isArray(data[i + 1])) { 27 | var d = { action: "insert", start: data[i], lines: data[i + 1] }; 28 | } else { 29 | var d = { action: "remove", start: data[i], end: data[i + 1] }; 30 | } 31 | doc.applyDelta(d, true); 32 | } 33 | } 34 | if (_self.$timeout) return deferredUpdate.schedule(_self.$timeout); 35 | _self.onUpdate(); 36 | }); 37 | }); 38 | 39 | (function () { 40 | this.$timeout = 500; 41 | 42 | this.setTimeout = function (timeout) { 43 | this.$timeout = timeout; 44 | }; 45 | 46 | this.setValue = function (value) { 47 | this.doc.setValue(value); 48 | this.deferredUpdate.schedule(this.$timeout); 49 | }; 50 | 51 | this.getValue = function (callbackId) { 52 | this.sender.callback(this.doc.getValue(), callbackId); 53 | }; 54 | 55 | this.onUpdate = function () { 56 | // abstract method 57 | }; 58 | 59 | this.isPending = function () { 60 | return this.deferredUpdate.isPending(); 61 | }; 62 | }.call(Mirror.prototype)); 63 | }); 64 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import { createCallbackAuth } from "@octokit/auth-callback"; 2 | import { Octokit } from "@octokit/rest"; 3 | import * as R from "ramda"; 4 | import { createStore, applyMiddleware, Store } from "redux"; 5 | import thunk from "redux-thunk"; 6 | import { createWrapStore, alias } from "webext-redux"; 7 | 8 | import createAliases from "./editor/aliases"; 9 | import { OCTOKIT_USER_AGENT } from "./editor/constants"; 10 | import errorMiddleware from "./editor/middleware/log-error"; 11 | import saveMiddleware from "./editor/middleware/save-when-inactive"; 12 | import settingsMiddleware from "./editor/middleware/settings"; 13 | import rootReducer, { RootState } from "./editor/reducers"; 14 | import { defaultState as defaultSettings } from "./editor/reducers/settings"; 15 | 16 | const chromeSyncStorageSetMerge = async (newStorage: any) => { 17 | const storage = await chrome.storage.sync.get(); 18 | const mergedValue = R.mergeDeepRight(storage, newStorage); 19 | await chrome.storage.sync.set(mergedValue); 20 | }; 21 | 22 | const settingsStorage = { 23 | set: (key: string, data: any) => 24 | chromeSyncStorageSetMerge({ 25 | settings: { 26 | [key]: data, 27 | }, 28 | }), 29 | }; 30 | 31 | const wrapStore = createWrapStore(); 32 | 33 | // TODO the interaction between octokit and the store is weird, can we untangle 34 | // this somehow? 35 | let store: Store | undefined; 36 | 37 | const octokit = new Octokit({ 38 | userAgent: OCTOKIT_USER_AGENT, 39 | authStrategy: createCallbackAuth, 40 | auth: { 41 | callback: () => store?.getState().settings.accessToken, 42 | }, 43 | }); 44 | 45 | Promise.all([ 46 | chrome.storage.sync.get({ settings: {} }), 47 | chrome.storage.session.get({ state: {} }), 48 | ]).then(([{ settings }, { state }]) => { 49 | store = createStore( 50 | rootReducer, 51 | Object.assign(state, { 52 | settings: Object.assign({}, defaultSettings, settings), 53 | }), 54 | applyMiddleware( 55 | alias(createAliases(octokit)), 56 | thunk, 57 | errorMiddleware, 58 | settingsMiddleware(settingsStorage), 59 | saveMiddleware 60 | ) 61 | ); 62 | wrapStore(store, { portName: "SNIPPETS" }); 63 | 64 | store.subscribe(() => { 65 | chrome.storage.session.set({ state: store!.getState() }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/editor/util/generate-redux.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreator, AnyAction } from "redux"; 2 | 3 | // TODO Move to separate file or use Ramda 4 | export const mapObject = ( 5 | callback: (key: K, value: V1) => V2, 6 | obj: { [key in K]: V1 } 7 | ): { [key in K]: V2 } => 8 | Object.entries(obj).reduce((newObj, [key, value]) => { 9 | newObj[key as K] = callback(key as K, value as V1); 10 | return newObj; 11 | }, {} as { [key in K]: V2 }); 12 | 13 | export type TypeDescription = { 14 | [name in T]: true | { action: ActionCreator }; 15 | }; 16 | 17 | export const generateTypes = ( 18 | typeDescription: TypeDescription 19 | ): { [name_ in T]: name_ } => mapObject((type) => type as any, typeDescription); 20 | 21 | export const generateActions = ( 22 | typeDescription: TypeDescription 23 | ): { [key in T]: (...args: any[]) => AnyAction } => 24 | mapObject( 25 | (type, definition) => 26 | typeof definition === "object" 27 | ? (...args: any[]) => 28 | Object.assign({ type: type }, definition.action(...args)) 29 | : (arg: any) => ({ type: type, [type]: arg }), 30 | typeDescription 31 | ); 32 | 33 | /** 34 | * Creates a reducer from individual update functions 35 | * 36 | * `updaters` should look like: 37 | * ``` 38 | * { 39 | * [actionType1]: (state, action) => ( 40 | * // update state 41 | * // the returned object is assigned to the current state 42 | * ) 43 | * } 44 | * ``` 45 | */ 46 | export const createReducer = ( 47 | updaters: { [name in T]: (state: S, action: AnyAction) => Partial }, 48 | defaultState: S 49 | ) => (state = defaultState, action: AnyAction) => { 50 | const updater = updaters[action.type as T]; 51 | return updater ? Object.assign({}, state, updater(state, action)) : state; 52 | }; 53 | 54 | /** 55 | * Creates a reducer based on the default state. Assumes each action has one 56 | * key which matches the `action.type`. That key's value will be placed into 57 | * the state using the same key. 58 | */ 59 | export const generateReducer = < 60 | T extends string, 61 | S extends { [key in T]: V }, 62 | V extends {} 63 | >( 64 | defaultState: S 65 | ) => 66 | createReducer( 67 | mapObject( 68 | (type) => (state, action) => ({ [type]: action[type] } as Partial), 69 | defaultState 70 | ), 71 | defaultState 72 | ); 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "2.5.0", 4 | "scripts": { 5 | "build:prod": "NODE_OPTIONS='--openssl-legacy-provider' webpack --mode=production --progress", 6 | "build:dev": "NODE_OPTIONS='--openssl-legacy-provider' webpack --progress", 7 | "start": "NODE_OPTIONS='--openssl-legacy-provider' webpack-dev-server --progress --env.devServer", 8 | "format": "prettier . --write", 9 | "tsc": "tsc --noEmit", 10 | "lint": "eslint . --ext js,ts,tsx", 11 | "lint:fix": "eslint . --ext js,ts,tsx --fix" 12 | }, 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@babel/core": "^7.4.3", 16 | "@babel/plugin-proposal-class-properties": "^7.4.0", 17 | "@babel/plugin-proposal-optional-chaining": "^7.13.12", 18 | "@babel/preset-env": "^7.4.3", 19 | "@babel/preset-react": "^7.0.0", 20 | "@babel/preset-typescript": "^7.13.0", 21 | "@types/chrome": "0.0.263", 22 | "@types/ramda": "^0.27.40", 23 | "@types/react-dom": "^16.8.5", 24 | "@types/react-redux": "^7.1.16", 25 | "@types/react-router-dom": "^5.1.7", 26 | "@types/react-router-transition": "^2.1.0", 27 | "@types/redux-logger": "^3.0.8", 28 | "@typescript-eslint/eslint-plugin": "^4.22.0", 29 | "@typescript-eslint/parser": "^4.22.0", 30 | "babel-loader": "^8.0.5", 31 | "clean-webpack-plugin": "^2.0.1", 32 | "confusing-browser-globals": "^1.0.10", 33 | "copy-webpack-plugin": "^6.0.3", 34 | "css-loader": "^2.1.1", 35 | "eslint": "^7.25.0", 36 | "eslint-plugin-import": "^2.22.1", 37 | "eslint-plugin-react": "^7.23.2", 38 | "eslint-plugin-react-hooks": "^4.2.0", 39 | "file-loader": "^3.0.1", 40 | "fork-ts-checker-webpack-plugin": "^6.2.5", 41 | "html-webpack-plugin": "^3.2.0", 42 | "mini-css-extract-plugin": "^0.6.0", 43 | "prettier": "^2.2.1", 44 | "redux-logger": "^3.0.6", 45 | "style-loader": "^0.23.1", 46 | "typescript": "^4.2.4", 47 | "url-loader": "^1.1.2", 48 | "webpack": "^4.44.1", 49 | "webpack-cli": "^3.3.12", 50 | "webpack-dev-server": "^3.11.0" 51 | }, 52 | "dependencies": { 53 | "@material-ui/core": "^3.9.3", 54 | "@material-ui/icons": "^3.0.2", 55 | "@octokit/auth-callback": "^2.0.1", 56 | "@octokit/rest": "^18.5.3", 57 | "ace-builds": "^1.4.12", 58 | "ramda": "^0.27.1", 59 | "react": "^16.8.6", 60 | "react-ace": "^9.4.0", 61 | "react-dom": "^16.8.6", 62 | "react-redux": "^7.2.4", 63 | "react-router-dom": "^5.2.0", 64 | "react-router-transition": "^2.1.0", 65 | "redux": "^4.1.0", 66 | "redux-thunk": "^2.3.0", 67 | "typeface-roboto": "0.0.54", 68 | "webext-redux": "3.0.0-mv3.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/timer.ts: -------------------------------------------------------------------------------- 1 | // The min and max values here define a range where both setTimeout and 2 | // chrome.alarms will be used for reliability. 3 | const MAX_SAFE_SET_TIMEOUT = 25 * 1000; 4 | const MIN_SAFE_ALARM = 30 * 1000; 5 | 6 | /** 7 | * Timer represents a delay that can be set and cancelled. It can only have one 8 | * delay at a time; calling `set` multiple times cancels the previous delay. 9 | * 10 | * The timer must be created synchronously when the service worker starts, to 11 | * guarantee that chrome alarm events will be received. 12 | * 13 | * A callback must be provided with `setCallback`. This may be done at any point 14 | * after creating the timer. The callback will be called for each time the timer 15 | * finished, even if the timer expired before the callback was set. 16 | * 17 | * Internally, delays are handled like so: 18 | * - If the delay is less than 25 seconds, setTimeout is used. 19 | * - If the delay is between 25 and 30 seconds, both setTimeout and chrome.alarms are 20 | * used. The callback will only be called once. 21 | * - If the delay is more than 30 seconds, only a chrome.alarms is used. 22 | * 23 | * Note that chrome alarms can only be set for a minimum of 30 seconds, but the 24 | * range between 25 and 30 is handled by both for reliability. 25 | * 26 | * `set` and `cancel` are async because the chrome alarms API is async. 27 | */ 28 | export const createTimer = (name: string) => { 29 | let timeout: ReturnType | undefined; 30 | 31 | let setCallback: (callback: () => void) => void; 32 | const callback: Promise<() => void> = new Promise((resolve) => { 33 | setCallback = resolve; 34 | }); 35 | 36 | /** 37 | * @returns Whether the timer was cancelled, `false` if no timer was running. 38 | */ 39 | const cancel = async () => { 40 | let cleared = false; 41 | 42 | if (timeout) { 43 | clearTimeout(timeout); 44 | timeout = undefined; 45 | cleared = true; 46 | } 47 | 48 | cleared = (await chrome.alarms.clear(name)) || cleared; 49 | 50 | return cleared; 51 | }; 52 | 53 | const onTimeout = async () => { 54 | await cancel(); 55 | // Awaiting the callback queues the event until the callback is ready 56 | (await callback)(); 57 | }; 58 | 59 | const onAlarm = async (alarm: chrome.alarms.Alarm) => { 60 | if (alarm.name !== name) { 61 | return; 62 | } 63 | 64 | await cancel(); 65 | // Awaiting the callback queues the event until the callback is ready 66 | (await callback)(); 67 | }; 68 | 69 | const set = async (delay: number) => { 70 | await cancel(); 71 | 72 | if (delay > MAX_SAFE_SET_TIMEOUT) { 73 | await chrome.alarms.create(name, { 74 | when: Math.max(Date.now() + delay, MIN_SAFE_ALARM), 75 | }); 76 | } 77 | 78 | if (delay < MIN_SAFE_ALARM) { 79 | timeout = setTimeout(onTimeout, delay); 80 | } 81 | }; 82 | 83 | chrome.alarms.onAlarm.addListener(onAlarm); 84 | 85 | return { 86 | set, 87 | cancel, 88 | // setCallback is guaranteed to exist because Promise runs its callback 89 | // synchronously. 90 | setCallback: setCallback!, 91 | }; 92 | }; 93 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | /* global localStorage */ 2 | 3 | /* This file starts the editor to allow for interface development in a 4 | regular webpage. It simulates panel.js (start editor) and background.js 5 | (redux store) 6 | */ 7 | 8 | import { createCallbackAuth } from "@octokit/auth-callback"; 9 | import { Octokit } from "@octokit/rest"; 10 | import { createStore, applyMiddleware, AnyAction } from "redux"; 11 | import { createLogger } from "redux-logger"; 12 | import thunk from "redux-thunk"; 13 | import { alias, Store } from "webext-redux"; 14 | 15 | import createEditor from "./editor"; 16 | import { types, Types as SettingsTypes } from "./editor/actions/settings"; 17 | import createAliases from "./editor/aliases"; 18 | import { OCTOKIT_USER_AGENT } from "./editor/constants"; 19 | import errorMiddleware from "./editor/middleware/log-error"; 20 | import saveMiddleware from "./editor/middleware/save-when-inactive"; 21 | import settingsMiddleware from "./editor/middleware/settings"; 22 | import rootReducer from "./editor/reducers"; 23 | import { 24 | defaultState as defaultSettings, 25 | SettingsState, 26 | } from "./editor/reducers/settings"; 27 | 28 | const LOCAL_STORAGE_PREFIX = "snippets-settings:"; 29 | 30 | const storage = { 31 | set: (path: string, data: unknown) => { 32 | localStorage[LOCAL_STORAGE_PREFIX + path] = JSON.stringify(data); 33 | }, 34 | }; 35 | 36 | // TODO the interaction between octokit and the store is weird, can we untangle 37 | // this somehow? 38 | const store = (() => { 39 | const octokit = new Octokit({ 40 | userAgent: OCTOKIT_USER_AGENT, 41 | authStrategy: createCallbackAuth, 42 | auth: { 43 | callback: () => store.getState().settings.accessToken, 44 | }, 45 | }); 46 | 47 | const store = createStore( 48 | rootReducer, 49 | { 50 | settings: Object.assign( 51 | defaultSettings, 52 | Object.keys(types).reduce((accum, key) => { 53 | const storedValue = localStorage.getItem(LOCAL_STORAGE_PREFIX + key); 54 | (accum as any)[key] = 55 | storedValue === null 56 | ? defaultSettings[key as SettingsTypes] 57 | : JSON.parse(storedValue); 58 | return accum; 59 | }, {} as SettingsState) 60 | ), 61 | }, 62 | applyMiddleware( 63 | alias(createAliases(octokit)), 64 | thunk, 65 | errorMiddleware, 66 | settingsMiddleware(storage), 67 | saveMiddleware, 68 | createLogger({ collapsed: true }) 69 | ) 70 | ); 71 | 72 | return store; 73 | })(); 74 | 75 | const delayedDispatch = (action: AnyAction) => { 76 | // Delay dispatches to the next tick simulate the asynchronous updates in 77 | // webext-redux 78 | setTimeout(() => { 79 | store.dispatch(action); 80 | }, 0); 81 | }; 82 | 83 | createEditor( 84 | // eslint-disable-next-line no-eval 85 | eval, 86 | "test-tab", 87 | new Proxy(store, { 88 | get: (target, key) => { 89 | if (key === "dispatch") { 90 | return delayedDispatch; 91 | } else { 92 | return (target as any)[key]; 93 | } 94 | }, 95 | }) as Store 96 | ); 97 | // Simulate webext-redux store load event 98 | store.dispatch({ type: "LOADED" }); 99 | -------------------------------------------------------------------------------- /src/editor/components/Login.tsx: -------------------------------------------------------------------------------- 1 | import Button from "@material-ui/core/Button"; 2 | import TextField from "@material-ui/core/TextField"; 3 | import React from "react"; 4 | import { connect } from "react-redux"; 5 | import { RouteComponentProps } from "react-router-dom"; 6 | 7 | import { actions as settingsActions } from "../actions/settings"; 8 | import { loadSnippets } from "../actions/snippets"; 9 | import { pages, CREATE_ACCESS_TOKEN_URL } from "../constants"; 10 | import { RootState } from "../reducers"; 11 | 12 | type Props = { 13 | gistId: string | false; 14 | settingsAccessToken: string | false; 15 | loadSnippets: () => void; 16 | accessToken: (accessToken: string) => void; 17 | }; 18 | 19 | type State = { 20 | accessTokenInput: string; 21 | }; 22 | 23 | class Login extends React.Component { 24 | state = { 25 | accessTokenInput: "", 26 | }; 27 | 28 | handleAccessTokenInput = (event: React.ChangeEvent) => { 29 | this.setState({ 30 | accessTokenInput: event.target.value, 31 | }); 32 | }; 33 | 34 | UNSAFE_componentWillReceiveProps({ 35 | settingsAccessToken: nextAccessToken, 36 | }: Props) { 37 | const { gistId, settingsAccessToken } = this.props; 38 | if (nextAccessToken && nextAccessToken !== settingsAccessToken) { 39 | if (gistId) { 40 | this.props.loadSnippets(); 41 | this.props.history.push(pages.MAIN); 42 | } else { 43 | this.props.history.push(pages.SELECT_GIST); 44 | } 45 | } 46 | } 47 | 48 | render() { 49 | return ( 50 |
51 |

Authenticate with Github

52 |

53 | In order to store snippets in a Github Gist, you'll need to provide a 54 | Github access token. It must have the "gist" scope. 55 |

56 | 65 |
72 | 78 | 85 |
86 |
87 | ); 88 | } 89 | } 90 | 91 | const mapStateToProps = ({ 92 | settings: { gistId, accessToken: settingsAccessToken }, 93 | }: RootState) => ({ 94 | gistId, 95 | settingsAccessToken, 96 | }); 97 | 98 | const mapDispatchToProps = { 99 | accessToken: settingsActions.accessToken, 100 | loadSnippets, 101 | }; 102 | 103 | export default connect(mapStateToProps, mapDispatchToProps)(Login); 104 | -------------------------------------------------------------------------------- /src/editor/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Roboto; 3 | font-size: 15px; 4 | margin: 0; 5 | } 6 | 7 | .home { 8 | display: flex; 9 | } 10 | 11 | .home header { 12 | display: flex; 13 | justify-content: flex-end; 14 | padding: 0 10px; 15 | background: #f3f3f3; 16 | } 17 | 18 | .none-selected { 19 | text-align: center; 20 | flex-grow: 1; 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: center; 24 | align-items: center; 25 | } 26 | .none-selected img { 27 | height: 100px; 28 | } 29 | 30 | .editor-container { 31 | flex-grow: 1; 32 | display: flex; 33 | flex-direction: column; 34 | } 35 | 36 | div.ace_editor { 37 | font-size: 1em; 38 | flex-grow: 1; 39 | } 40 | 41 | .sidepane { 42 | width: 12%; 43 | min-width: 200px; 44 | height: 100vh; 45 | background-color: #f3f3f3; 46 | display: flex; 47 | flex-direction: column; 48 | } 49 | 50 | .sidepane .selected { 51 | background: lightgray; 52 | cursor: default; 53 | } 54 | .sidepane .menu-icon { 55 | opacity: 0; 56 | transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; 57 | } 58 | .sidepane li:hover .menu-icon, 59 | .sidepane .selected + div .menu-icon { 60 | opacity: 1; 61 | } 62 | 63 | .snippet-list { 64 | margin-top: 10px; 65 | flex-grow: 1; 66 | overflow: auto; 67 | } 68 | 69 | .snippet-row { 70 | background-color: transparent; 71 | padding: 10px; 72 | font-size: 1.2em; 73 | margin-left: 20px; 74 | cursor: pointer; 75 | transition: background-color 300ms; 76 | } 77 | .snippet-row.selected { 78 | background-color: #d8d8d8; 79 | cursor: default; 80 | border-radius: 0 0 2px 2px; 81 | } 82 | .snippet-row:not(.selected):hover { 83 | background-color: #ffffff; 84 | } 85 | 86 | .sidepane input { 87 | width: 100%; 88 | } 89 | 90 | button.create { 91 | background-color: #4c6ffc; 92 | color: white; 93 | padding: 10px 20px; 94 | margin: 10px auto; 95 | outline: none; 96 | border: none; 97 | border-radius: 4px; 98 | display: block; 99 | cursor: pointer; 100 | transition: background-color 0.2s; 101 | } 102 | button.create:hover { 103 | background-color: #a3b5ff; 104 | } 105 | 106 | button.delete { 107 | background-color: #fc4c4c; 108 | color: white; 109 | padding: 5px 15px; 110 | margin: 10px auto; 111 | outline: none; 112 | border: none; 113 | border-radius: 4px; 114 | display: block; 115 | cursor: pointer; 116 | transition: background-color 0.2s; 117 | } 118 | button.delete:hover { 119 | background-color: #ff7c7c; 120 | } 121 | 122 | .save-error { 123 | color: red; 124 | text-decoration: none; 125 | } 126 | .error { 127 | margin: 50px auto; 128 | max-width: 600px; 129 | font-family: sans-serif; 130 | font-size: 1.5em; 131 | text-align: center; 132 | } 133 | .error h1, 134 | .error p, 135 | .error pre, 136 | .error a { 137 | text-align: center; 138 | } 139 | .error pre { 140 | width: 100%; 141 | color: red; 142 | white-space: pre-wrap; 143 | } 144 | .error h1 { 145 | margin: 10px 0 0; 146 | } 147 | 148 | .error a { 149 | margin-top: 20px; 150 | } 151 | 152 | .error .action, 153 | .error a { 154 | font-size: 0.7em; 155 | display: block; 156 | } 157 | 158 | .switch-wrapper { 159 | position: relative; 160 | } 161 | .switch-wrapper > div { 162 | position: absolute; 163 | left: 0; 164 | right: 0; 165 | top: 0; 166 | bottom: 0; 167 | } 168 | -------------------------------------------------------------------------------- /src/editor/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ExpansionPanel, 3 | ExpansionPanelSummary, 4 | ExpansionPanelDetails, 5 | Button, 6 | } from "@material-ui/core"; 7 | import ErrorIcon from "@material-ui/icons/ErrorOutline"; 8 | import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; 9 | import React from "react"; 10 | import ReactDOM from "react-dom"; 11 | import { Provider } from "react-redux"; 12 | import { Store } from "webext-redux"; 13 | 14 | import App from "./components/App"; 15 | 16 | type State = { 17 | caughtError: Error | null; 18 | }; 19 | 20 | class ErrorBoundary extends React.Component<{}, State> { 21 | constructor(props: any) { 22 | super(props); 23 | 24 | this.state = { caughtError: null }; 25 | } 26 | 27 | static getDerivedStateFromError(error: Error) { 28 | return { caughtError: error }; 29 | } 30 | 31 | render() { 32 | if (this.state.caughtError) { 33 | return ( 34 |
44 | 45 |

An error occurred

46 | 54 | }> 55 | Error Details 56 | 57 | 58 |
{this.state.caughtError.stack}
59 |
60 |
61 |
62 | Please report it{" "} 63 | 68 | on GitHub 69 | 70 |
71 | 78 |
79 | ); 80 | } else { 81 | return this.props.children; 82 | } 83 | } 84 | } 85 | 86 | const chromeReduxStore = () => 87 | new Store({ 88 | portName: "SNIPPETS", 89 | }); 90 | 91 | export default ( 92 | runInInspectedWindow: (code: string) => void, 93 | editorId: string, 94 | store = chromeReduxStore() 95 | ) => { 96 | // Ignore the first event; the store will be empty 97 | const unsubscribe = store.subscribe(() => { 98 | unsubscribe(); 99 | const root = document.querySelector(".root"); 100 | ReactDOM.render( 101 | 102 | 103 | 107 | 108 | , 109 | root 110 | ); 111 | }); 112 | }; 113 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.5.0 2 | 3 | - Updated extension for manifest v3, related to APIs and environment exposed by Chrome. [More details in #32](https://github.com/SidneyNemzer/snippets/issues/32). 4 | - Resolved an issue that prevented the linter from working. 5 | - Fixed an error related to renaming or deleting a snippet. 6 | 7 | ## 2.4.0 8 | 9 | - Added the option to create a new Gist during setup instead of using an existing Gist 10 | - Resolved an issue where the Esc shortcut did not open the devtools console 11 | - Fixed an error that occurred when deleting the first snippet in the list ([#26](https://github.com/SidneyNemzer/snippets/issues/26)) 12 | - Resolved an error that occurred deleting a snippet that had never been saved 13 | - Re-implemented a fade animation for the settings page 14 | - Rewrote the extension with TypeScript 15 | - Updated packages 16 | - Minor animation changes 17 | 18 | ## 2.3.0 19 | 20 | - Switched to ESLint for better support of new JavaScript syntax. ESLint will check for syntax errors, and I've enabled a few rules that I think are relevant for Snippets. [View the rules here][eslint-rules]. These could be configurable in the future. 21 | - In the settings, dropdowns are now used where applicable 22 | - Added an input field to view your access token (hidden by default) 23 | 24 | [eslint-rules]: https://github.com/SidneyNemzer/snippets/blob/4541e82082ac49070c338abba6c3298f96523665/src/mode-javascript-eslint/worker-javascript-eslint.js#L19-L33 25 | 26 | ## 2.2.0 27 | 28 | - Editor font size is now configurable in the settings, thanks @mighty-sparrow! 29 | - Fixed a bug where the selected snippet reset when opening settings 30 | - Lots of code refactoring, React error boundary 31 | 32 | ## 2.1.1 33 | 34 | - Fixed a bug where empty snippets would fail to save. Thanks to @Pfennigbaum for reporting! 35 | - Addressed a bug where typing a question mark would open the devtool's settings 36 | - Fixed a bug where selecting a different snippet added to the undo history 37 | - Fixed an issue that could causes the cursor to jump while typing quickly 38 | - Updated dependencies 39 | 40 | ## 2.1.0 41 | 42 | - The _Run_ button and shortcut (Ctrl/Cmd + Enter) didn't work. Thanks to @bladnman for reporting! 43 | - An error message is now logged if you run a Snippet with a syntax error 44 | 45 | ## 2.0.0 46 | 47 | - Snippets are now stored in a Github Gist instead of Chrome sync storage. This addresses the low data limit of Chrome sync storage. 48 | - Added a setting to configure autosave time 49 | - Slightly reduced editor font size 50 | - Fixed an issue where the code editor (Ace) wouldn't get shorter than 500px 51 | 52 | ## 1.1.2 53 | 54 | - A warning will be displayed when the storage limit is exceeded 55 | 56 | ## 1.1.1 57 | 58 | - Fixed an issue with loading legacy snippet data 59 | 60 | ## 1.1.0 61 | 62 | - Added options to control the linter and line wrapping 63 | - Fixed an issue where old snippet data overwrote new data 64 | - Slightly reduced font size 65 | - Code cleanup 66 | 67 | ## 1.0.1 68 | 69 | - Fixed an issue with loading legacy snippet data 70 | 71 | ## 1.0.0 72 | 73 | - Added configurable settings 74 | - Added autocompletion 75 | - Added autosave (everything is autosaved!) 76 | - Updated interface 77 | - Errors are displayed in nicer page. _Note: fixed all bugs and errors_. 78 | - Fixed a spelling mistake in extension's description 79 | - Disabled line highlighting 80 | - Errors in snippets are displayed in the console instead of being hidden 81 | - Updated the "Welcome!" snippet 82 | - Defaulted indent to 2 spaces instead of 4 83 | 84 | ## 0.1.1 85 | 86 | - Addressed an issue that caused an error when saving or loading snippets after installing extension 87 | - New snippets are marked as unsaved 88 | 89 | ## 0.1.0 90 | 91 | _Initial Release_ 92 | -------------------------------------------------------------------------------- /src/editor/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-webpack-loader-syntax */ 2 | /* global ace */ 3 | 4 | // ace-builds must be imported before react-ace 5 | // See https://github.com/securingsincity/react-ace/issues/725 6 | import "ace-builds"; 7 | 8 | import React from "react"; 9 | import AceEditor from "react-ace"; 10 | import { connect } from "react-redux"; 11 | 12 | import "ace-builds/src-noconflict/ext-language_tools"; 13 | import "ace-builds/src-noconflict/mode-javascript"; 14 | import "ace-builds/src-noconflict/theme-github"; 15 | import "ace-builds/src-noconflict/theme-tomorrow_night"; 16 | 17 | import { RootState } from "../reducers"; 18 | import { SettingsState } from "../reducers/settings"; 19 | 20 | // Tell webpack to include these files in the build. file-loader replaces 21 | // these `require`'s with a string, the path to the file in the build output. 22 | // Ace loads these dynamically at runtime. We don't use ace-builds/webpack-resolver 23 | // because that pulls in the files for EVERY mode, theme, etc. 24 | ace.config.setModuleUrl( 25 | "ace/ext/language_tools", 26 | require("file-loader?esModule=false&name=[name].[ext]!ace-builds/src-noconflict/ext-language_tools.js") 27 | ); 28 | ace.config.setModuleUrl( 29 | "ace/mode/javascript", 30 | require("file-loader?esModule=false&name=[name].[ext]!ace-builds/src-noconflict/mode-javascript.js") 31 | ); 32 | 33 | // Custom mode that uses ESLint 34 | ace.config.setModuleUrl( 35 | "ace/mode/javascript-eslint", 36 | require("file-loader?esModule=false&name=[name].[ext]!../../mode-javascript-eslint/mode-javascript-eslint.js") 37 | ); 38 | 39 | type Props = { 40 | value: string; 41 | editorId: string; 42 | lastUpdatedBy: string | undefined; 43 | settings: SettingsState; 44 | onChange: (value: string) => void; 45 | }; 46 | 47 | type State = { 48 | value: string; 49 | }; 50 | 51 | class Editor extends React.Component { 52 | constructor(props: Props) { 53 | super(props); 54 | this.state = { value: props.value }; 55 | } 56 | 57 | static getDerivedStateFromProps(props: Props, state: State) { 58 | if (props.lastUpdatedBy && props.lastUpdatedBy !== props.editorId) { 59 | return { value: props.value }; 60 | } else { 61 | return null; 62 | } 63 | } 64 | 65 | handleChange = (value: string) => { 66 | this.setState({ value }); 67 | this.props.onChange(value); 68 | }; 69 | 70 | render() { 71 | return ( 72 | { 94 | ace.container.addEventListener("keydown", (event) => { 95 | // Allow Esc to open the devtools console. Internally, the devtools 96 | // ignore the event if a textarea or input is focused 97 | if ( 98 | event.key === "Escape" && 99 | document.activeElement instanceof HTMLElement 100 | ) { 101 | document.activeElement.blur(); 102 | } 103 | 104 | // Prevent devtools from handling question marks, otherwise typing 105 | // a question mark would not be possible. 106 | if (event.key === "?") { 107 | event.stopPropagation(); 108 | } 109 | }); 110 | }} 111 | /> 112 | ); 113 | } 114 | } 115 | 116 | const mapStateToProps = (state: RootState) => ({ 117 | settings: state.settings, 118 | }); 119 | 120 | export default connect(mapStateToProps)(Editor); 121 | -------------------------------------------------------------------------------- /src/editor/aliases.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Redux actions that are usually thunks must be implemented with aliases 3 | * https://github.com/tshaddix/webext-redux/wiki/Advanced-Usage 4 | */ 5 | 6 | import { Octokit } from "@octokit/rest"; 7 | import { Dispatch } from "redux"; 8 | 9 | import { 10 | LOAD_SNIPPETS, 11 | LOADING_SNIPPETS, 12 | LOAD_LEGACY_SNIPPETS, 13 | LOADED_LEGACY_SNIPPETS, 14 | SAVE_SNIPPETS, 15 | SAVING_SNIPPETS, 16 | loadedSnippets, 17 | savedSnippets, 18 | } from "./actions/snippets"; 19 | import { RootState } from "./reducers"; 20 | 21 | type Snippet = { 22 | name: string; 23 | body: string; 24 | }; 25 | 26 | const loadSnippets = (octokit: Octokit) => () => ( 27 | dispatch: Dispatch, 28 | getState: () => RootState 29 | ) => { 30 | const gistId = getState().settings.gistId; 31 | if (!gistId) { 32 | const error: any = new Error("Missing Gist ID"); 33 | error.context = "load snippets"; 34 | dispatch(loadedSnippets(error)); 35 | return; 36 | } 37 | 38 | const loading = getState().snippets.loading; 39 | if (loading) { 40 | return; 41 | } 42 | 43 | dispatch({ type: LOADING_SNIPPETS }); 44 | 45 | octokit.gists 46 | .get({ gist_id: gistId }) 47 | .then(({ data: gist }) => { 48 | dispatch( 49 | loadedSnippets( 50 | null, 51 | Object.entries(gist.files || {}).reduce( 52 | (snippets, [fileName, gistFile]) => { 53 | if (!gistFile) { 54 | return snippets; 55 | } 56 | 57 | const { truncated, content } = gistFile; 58 | 59 | snippets[fileName] = { 60 | name: fileName, 61 | // TODO handle truncated files 62 | body: truncated ? "(Truncated)" : content || "", 63 | }; 64 | return snippets; 65 | }, 66 | {} as { [name: string]: Snippet } 67 | ) 68 | ) 69 | ); 70 | }) 71 | .catch((error) => { 72 | error.context = "load snippets"; 73 | dispatch(loadedSnippets(error)); 74 | }); 75 | }; 76 | 77 | type GistFile = { 78 | content?: string; 79 | filename?: string; 80 | }; 81 | 82 | const saveSnippets = (octokit: Octokit) => () => ( 83 | dispatch: Dispatch, 84 | getState: () => RootState 85 | ) => { 86 | const { 87 | snippets: { data }, 88 | settings: { gistId }, 89 | } = getState(); 90 | 91 | if (!data) return; 92 | 93 | dispatch({ type: SAVING_SNIPPETS }); 94 | 95 | const files = Object.entries(data).reduce((files, [name, snippet]) => { 96 | if (snippet.deleted) { 97 | files[name] = null; 98 | } else if (snippet.content.local !== snippet.content.remote) { 99 | const content = snippet.content.local.trim() 100 | ? snippet.content.local 101 | : "(Github Gists can't be empty so Snippets saved this content)"; 102 | 103 | files[name] = { content }; 104 | } 105 | 106 | if (snippet.renamed) { 107 | files[name] = files[name] || {}; 108 | (files[name] as GistFile).filename = snippet.renamed; 109 | } 110 | 111 | return files; 112 | }, {} as { [name: string]: GistFile | null }); 113 | 114 | octokit.gists 115 | .update({ gist_id: gistId, files }) 116 | .then(() => dispatch(savedSnippets(null))) 117 | .catch((error) => { 118 | error.context = "save snippets"; 119 | dispatch(savedSnippets(error)); 120 | }); 121 | }; 122 | 123 | const loadLegacySnippets = () => async (dispatch: Dispatch) => { 124 | const storage = await chrome.storage.sync.get(); 125 | const snippets: Record< 126 | string, 127 | { content?: string; body?: string; name: string } 128 | > = storage.snippets; 129 | const processedSnippets = Object.entries(snippets).reduce( 130 | (snippets, [id, value]) => { 131 | const { content, body, name } = value; 132 | if (body || content) { 133 | snippets[name] = body || content || ""; 134 | } 135 | return snippets; 136 | }, 137 | {} as { [name: string]: string } 138 | ); 139 | 140 | dispatch({ 141 | type: LOADED_LEGACY_SNIPPETS, 142 | error: null, 143 | snippets: processedSnippets, 144 | }); 145 | }; 146 | 147 | export default (octokit: Octokit) => ({ 148 | [LOAD_SNIPPETS]: loadSnippets(octokit), 149 | [SAVE_SNIPPETS]: saveSnippets(octokit), 150 | [LOAD_LEGACY_SNIPPETS]: loadLegacySnippets, 151 | }); 152 | -------------------------------------------------------------------------------- /src/mode-javascript-eslint/worker-javascript-eslint.js: -------------------------------------------------------------------------------- 1 | /* global ace */ 2 | 3 | import "./worker"; // defines globals like `window` 4 | import "ace-builds"; // defines all ace modules with `ace.define` 5 | import "./mirror"; // defines `ace/worker/mirror` which is not used by the main ace script 6 | 7 | // ESLint usage is based on the official demo 8 | // https://github.com/eslint/website/blob/5a3ffe7e67eca55675b6e30029891da9098c1e6c/src/js/demo/components/App.jsx 9 | 10 | import eslint from "eslint/lib/linter/linter"; 11 | 12 | const linter = new eslint.Linter(); 13 | 14 | // ESLint allows rules to be 'warning' or 'error' but Ace also has 'info', 15 | // this object defines the type of each rule. These are subjective but 16 | // generally errors are more likely to indicate a problem. Rules like 17 | // `no-undef` normally indicate an error but Snippets are likely to depend on 18 | // values defined in the page. 19 | const ruleAnnotationType = { 20 | "no-dupe-args": "warning", 21 | "no-dupe-else-if": "warning", 22 | "no-dupe-keys": "warning", 23 | "no-duplicate-case": "warning", 24 | "no-template-curly-in-string": "warning", 25 | "no-unexpected-multiline": "error", 26 | "use-isnan": "error", 27 | "valid-typeof": "warning", 28 | "no-new-wrappers": "warning", 29 | "no-undef": "info", 30 | "no-unused-vars": "info", 31 | "no-const-assign": "error", 32 | "no-dupe-class-members": "warning", 33 | }; 34 | 35 | ace.define( 36 | "ace/mode/javascript_worker_eslint", 37 | function (require, exports, module) { 38 | const oop = require("ace/lib/oop"); 39 | const Mirror = require("ace/worker/mirror").Mirror; 40 | 41 | const JavaScriptWorkerEslint = function (sender) { 42 | Mirror.call(this, sender); 43 | this.setTimeout(500); 44 | this.setOptions(); 45 | }; 46 | 47 | oop.inherits(JavaScriptWorkerEslint, Mirror); 48 | 49 | (function () { 50 | this.setOptions = function (options) { 51 | this.options = options || { 52 | parserOptions: { 53 | ecmaVersion: "2020", 54 | sourceType: "script", 55 | }, 56 | env: { browser: true, es6: true, es2017: true, es2020: true }, 57 | rules: { 58 | // We define the error level separately so everything is set to 59 | // 'warning' here (`1`). 60 | "no-dupe-args": 1, 61 | "no-dupe-else-if": 1, 62 | "no-dupe-keys": 1, 63 | "no-duplicate-case": 1, 64 | "no-template-curly-in-string": 1, 65 | "no-unexpected-multiline": 1, 66 | "use-isnan": 1, 67 | "valid-typeof": 1, 68 | "no-new-wrappers": 1, 69 | "no-undef": 1, 70 | "no-unused-vars": 1, 71 | "no-const-assign": 1, 72 | "no-dupe-class-members": 1, 73 | }, 74 | }; 75 | this.doc.getValue() && this.deferredUpdate.schedule(100); 76 | }; 77 | 78 | this.changeOptions = function (newOptions) { 79 | oop.mixin(this.options, newOptions); 80 | this.doc.getValue() && this.deferredUpdate.schedule(100); 81 | }; 82 | 83 | this.onUpdate = function () { 84 | var value = this.doc.getValue(); 85 | 86 | if (!value) { 87 | return this.sender.emit("annotate", []); 88 | } 89 | 90 | const annotations = []; 91 | 92 | try { 93 | const messages = linter.verify(value, this.options); 94 | 95 | for (let i = 0; i < messages.length; i++) { 96 | const message = messages[i]; 97 | annotations.push({ 98 | row: message.line - 1, 99 | column: message.column - 1, 100 | text: `${message.message}${ 101 | message.ruleId ? `\n${message.ruleId}` : "" 102 | }`, 103 | // parsing errors don't have a rule ID 104 | type: message.ruleId 105 | ? ruleAnnotationType[message.ruleId] 106 | : "error", 107 | }); 108 | } 109 | } catch (error) { 110 | annotations.push({ 111 | row: 0, 112 | column: 0, 113 | text: `An error occurred while linting:\n${error.stack}`, 114 | type: "error", 115 | }); 116 | } 117 | 118 | this.sender.emit("annotate", annotations); 119 | }; 120 | }.call(JavaScriptWorkerEslint.prototype)); 121 | 122 | exports.JavaScriptWorkerEslint = JavaScriptWorkerEslint; 123 | } 124 | ); 125 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | const CleanWebpackPlugin = require("clean-webpack-plugin"); 5 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 6 | const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); 7 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 8 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 9 | const webpack = require("webpack"); 10 | 11 | const packageJson = require("./package.json"); 12 | const manifestJson = require("./src/manifest.json"); 13 | 14 | const AfterEmitPlugin = (fn) => ({ 15 | apply: (compiler) => { 16 | compiler.hooks.afterEmit.tap("AfterEmitPlugin", fn); 17 | }, 18 | }); 19 | 20 | const buildManifest = () => { 21 | const manifest = Object.assign(manifestJson, { 22 | version: packageJson.version, 23 | }); 24 | fs.writeFileSync("build/manifest.json", JSON.stringify(manifest, null, 2)); 25 | }; 26 | 27 | const html = [ 28 | new HtmlWebpackPlugin({ 29 | chunks: ["panel"], 30 | filename: "panel.html", 31 | title: "Snippets", 32 | template: "./src/panel.html", 33 | }), 34 | new HtmlWebpackPlugin({ 35 | chunks: ["devtools"], 36 | filename: "devtools.html", 37 | title: "Snippets", 38 | }), 39 | ]; 40 | 41 | const devServerHtml = [ 42 | new HtmlWebpackPlugin({ 43 | template: "./src/panel.html", 44 | chunks: ["test"], 45 | filename: "index.html", 46 | }), 47 | ]; 48 | 49 | const extractCssLoader = { 50 | loader: MiniCssExtractPlugin.loader, 51 | options: { hmr: false }, 52 | }; 53 | 54 | module.exports = (env, args) => { 55 | const isDevServer = env && env.devServer; 56 | const isProduction = args.mode === "production"; 57 | const entry = isDevServer 58 | ? { 59 | test: "./src/test.ts", 60 | "worker-javascript-eslint": 61 | "./src/mode-javascript-eslint/worker-javascript-eslint.js", 62 | } 63 | : { 64 | background: "./src/background.ts", 65 | devtools: "./src/devtools.ts", 66 | panel: "./src/panel.ts", 67 | "worker-javascript-eslint": 68 | "./src/mode-javascript-eslint/worker-javascript-eslint.js", 69 | }; 70 | 71 | return { 72 | mode: args.mode || "development", 73 | 74 | devtool: !isProduction && "cheap-source-map", 75 | 76 | entry, 77 | 78 | output: { 79 | path: path.resolve(__dirname, "build"), 80 | }, 81 | 82 | module: { 83 | strictExportPresence: true, 84 | rules: [ 85 | { 86 | oneOf: [ 87 | { 88 | test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], 89 | loader: "url-loader", 90 | options: { 91 | limit: 10000, 92 | fallback: "file-loader", 93 | name: "[name].[ext]", 94 | }, 95 | }, 96 | { 97 | test: /\.(jsx?|tsx?)$/, 98 | loader: "babel-loader", 99 | options: { cacheDirectory: true }, 100 | }, 101 | { 102 | test: /\.css$/, 103 | use: [ 104 | isProduction ? extractCssLoader : "style-loader", 105 | "css-loader", 106 | ], 107 | }, 108 | { 109 | test: /\.(ttf|eot|woff|woff2)$/, 110 | loader: "file-loader", 111 | options: { name: "fonts/[name].[ext]" }, 112 | }, 113 | { 114 | // Exclude a few other extensions so they get processed by Webpack's 115 | // internal loaders. 116 | exclude: [/\.js$/, /\.html$/, /\.json$/, /\.ejs$/], 117 | loader: "file-loader", 118 | options: { name: "[name].[ext]" }, 119 | }, 120 | ], 121 | }, 122 | ], 123 | }, 124 | 125 | plugins: [ 126 | new webpack.DefinePlugin({ 127 | "process.env.SNIPPETS_VERSION": JSON.stringify(packageJson.version), 128 | "process.env.NODE_ENV": JSON.stringify(args.mode || "development"), 129 | }), 130 | new CleanWebpackPlugin(), 131 | new CopyWebpackPlugin({ patterns: [{ from: "static", to: "./" }] }), 132 | AfterEmitPlugin(buildManifest), 133 | isProduction && new MiniCssExtractPlugin(), 134 | ...(isDevServer ? devServerHtml : html), 135 | isDevServer && new ForkTsCheckerWebpackPlugin(), 136 | ].filter(Boolean), 137 | 138 | optimization: { 139 | minimize: isProduction, 140 | }, 141 | 142 | performance: { 143 | hints: false, 144 | }, 145 | 146 | resolve: { 147 | extensions: [".js", ".jsx", ".ts", ".tsx"], 148 | mainFields: ["browser", "main", "module"], 149 | }, 150 | 151 | node: { 152 | module: "empty", 153 | dgram: "empty", 154 | dns: "mock", 155 | fs: "empty", 156 | http2: "empty", 157 | net: "empty", 158 | tls: "empty", 159 | child_process: "empty", 160 | }, 161 | }; 162 | }; 163 | -------------------------------------------------------------------------------- /src/editor/components/SnippetSelector.tsx: -------------------------------------------------------------------------------- 1 | import IconButton from "@material-ui/core/IconButton"; 2 | import ListItem from "@material-ui/core/ListItem"; 3 | import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction"; 4 | import ListItemText from "@material-ui/core/ListItemText"; 5 | import Menu from "@material-ui/core/Menu"; 6 | import MenuItem from "@material-ui/core/MenuItem"; 7 | import Delete from "@material-ui/icons/Delete"; 8 | import Edit from "@material-ui/icons/Edit"; 9 | import MoreVert from "@material-ui/icons/MoreVert"; 10 | import PlayArrow from "@material-ui/icons/PlayArrow"; 11 | import React from "react"; 12 | 13 | type Props = { 14 | name: string; 15 | selected: boolean; 16 | updateName: (name: string) => void; 17 | selectSnippet: () => void; 18 | runSnippet: () => void; 19 | deleteSnippet: () => void; 20 | }; 21 | 22 | type State = { 23 | isRenaming: boolean; 24 | startingRename: boolean; 25 | currentInput: string; 26 | menuAnchor: HTMLElement | null; 27 | menuOpen: boolean; 28 | }; 29 | 30 | class SnippetSelector extends React.Component { 31 | nameInput: HTMLInputElement | undefined; 32 | 33 | constructor(props: Props) { 34 | super(props); 35 | 36 | this.state = { 37 | isRenaming: false, 38 | startingRename: false, 39 | currentInput: "", 40 | menuAnchor: null, 41 | menuOpen: false, 42 | }; 43 | 44 | this.startRename = this.startRename.bind(this); 45 | this.handleChange = this.handleChange.bind(this); 46 | this.handleBlur = this.handleBlur.bind(this); 47 | this.handleKeyPress = this.handleKeyPress.bind(this); 48 | this.handleMenuOpen = this.handleMenuOpen.bind(this); 49 | this.handleMenuClose = this.handleMenuClose.bind(this); 50 | } 51 | 52 | startRename() { 53 | this.setState({ 54 | isRenaming: true, 55 | startingRename: true, 56 | currentInput: this.props.name, 57 | menuOpen: false, 58 | }); 59 | } 60 | 61 | handleChange(event: React.ChangeEvent) { 62 | this.setState({ 63 | currentInput: event.target.value, 64 | startingRename: false, 65 | }); 66 | } 67 | 68 | handleBlur() { 69 | const { currentInput } = this.state; 70 | this.setState( 71 | { 72 | isRenaming: false, 73 | }, 74 | () => { 75 | this.props.updateName(currentInput); 76 | } 77 | ); 78 | } 79 | 80 | handleKeyPress(event: React.KeyboardEvent) { 81 | // End the rename when 'enter' is hit 82 | if (event.key === "Enter") { 83 | this.nameInput?.blur(); 84 | } 85 | } 86 | 87 | handleMenuOpen(event: React.MouseEvent) { 88 | this.setState({ 89 | menuAnchor: event.currentTarget, 90 | menuOpen: true, 91 | }); 92 | } 93 | 94 | handleMenuClose() { 95 | this.setState({ 96 | menuOpen: false, 97 | }); 98 | } 99 | 100 | componentDidUpdate() { 101 | if (this.state.isRenaming && this.state.startingRename) { 102 | // Without the delay, the input instantly loses focus... 103 | // not sure why 104 | setTimeout(() => { 105 | this.nameInput?.select(); 106 | this.nameInput?.focus(); 107 | }, 350); // With any lower delay, 'nameInput' is undefined 108 | } 109 | } 110 | 111 | renderName() { 112 | const { isRenaming } = this.state; 113 | const { name } = this.props; 114 | 115 | if (isRenaming) { 116 | // TODO use material ui input 117 | return ( 118 | { 120 | this.nameInput = input || undefined; 121 | }} 122 | value={this.state.currentInput} 123 | onChange={this.handleChange} 124 | onBlur={this.handleBlur} 125 | onKeyPress={this.handleKeyPress} 126 | /> 127 | ); 128 | } else { 129 | return ; 130 | } 131 | } 132 | 133 | render() { 134 | const { selected } = this.props; 135 | 136 | // TODO maybe disable ListItem.button and Menu icon when renaming 137 | return ( 138 | {} : () => this.props.selectSnippet()} 141 | className={selected ? "selected" : ""} 142 | > 143 | {this.renderName()} 144 | 145 | { 148 | if (!selected) { 149 | this.props.selectSnippet(); 150 | } 151 | 152 | this.handleMenuOpen(event); 153 | }} 154 | > 155 | 156 | 157 | 162 | 163 | 164 | Rename 165 | 166 | 167 | 168 | Run 169 | 170 | 171 | 172 | Delete 173 | 174 | 175 | 176 | 177 | ); 178 | } 179 | } 180 | 181 | export default SnippetSelector; 182 | -------------------------------------------------------------------------------- /src/editor/components/SelectGist.tsx: -------------------------------------------------------------------------------- 1 | import { FormControlLabel, RadioGroup } from "@material-ui/core"; 2 | import Button from "@material-ui/core/Button"; 3 | import CircularProgress from "@material-ui/core/CircularProgress"; 4 | import Radio from "@material-ui/core/Radio"; 5 | import TextField from "@material-ui/core/TextField"; 6 | import { Octokit } from "@octokit/rest"; 7 | import React from "react"; 8 | import { connect } from "react-redux"; 9 | import { RouteComponentProps } from "react-router-dom"; 10 | 11 | import { actions as settingsActions } from "../actions/settings"; 12 | import { loadSnippets } from "../actions/snippets"; 13 | import { 14 | OCTOKIT_USER_AGENT, 15 | pages, 16 | WELCOME_SNIPPET_CONTENT, 17 | } from "../constants"; 18 | import { RootState } from "../reducers"; 19 | 20 | const usersGists = "https://gist.github.com/"; 21 | 22 | type Props = { 23 | accessToken: string | false; 24 | settingsGistId: string | false; 25 | loadSnippets: () => void; 26 | gistId: (gistId: string) => void; 27 | }; 28 | 29 | type State = { 30 | gistType: "new" | "custom"; 31 | gistIdInput: string; 32 | creatingGist: boolean; 33 | createGistError: Error | undefined; 34 | }; 35 | 36 | class SelectGist extends React.Component { 37 | state = { 38 | gistType: "new", 39 | gistIdInput: "", 40 | creatingGist: false, 41 | createGistError: undefined, 42 | } as State; 43 | 44 | handleGistInput = (event: React.ChangeEvent) => { 45 | this.setState({ 46 | gistIdInput: event.target.value, 47 | }); 48 | }; 49 | 50 | handleGistType = (event: React.ChangeEvent) => { 51 | this.setState({ 52 | gistType: event.target.value as State["gistType"], 53 | }); 54 | }; 55 | 56 | handleCreateGist = () => { 57 | this.setState({ creatingGist: true, createGistError: undefined }); 58 | 59 | const octokit = new Octokit({ 60 | userAgent: OCTOKIT_USER_AGENT, 61 | auth: this.props.accessToken, 62 | }); 63 | 64 | octokit.gists 65 | .create({ 66 | description: "Snippets", 67 | files: { Welcome: { content: WELCOME_SNIPPET_CONTENT } }, 68 | public: false, 69 | }) 70 | .then(({ data: gist }) => { 71 | if (!gist.id) { 72 | throw new Error("Invalid response from GitHub"); 73 | } 74 | this.props.gistId(gist.id); 75 | }) 76 | .catch((error) => { 77 | this.setState({ createGistError: error }); 78 | }) 79 | .finally(() => { 80 | this.setState({ creatingGist: false }); 81 | }); 82 | }; 83 | 84 | UNSAFE_componentWillReceiveProps({ settingsGistId: nextGistId }: Props) { 85 | const { accessToken, settingsGistId } = this.props; 86 | if (nextGistId && nextGistId !== settingsGistId) { 87 | if (accessToken) { 88 | this.props.loadSnippets(); 89 | this.props.history.push(pages.MAIN); 90 | } else { 91 | this.props.history.push(pages.LOGIN); 92 | } 93 | } 94 | } 95 | 96 | loadGist = () => { 97 | this.props.gistId(this.state.gistIdInput); 98 | }; 99 | 100 | render() { 101 | return ( 102 |
103 |

Choose a Gist to store Snippets

104 |

105 | 106 | Click here to open GitHub Gists 107 | 108 |

109 | 113 | } 116 | label="Create new secret Gist" 117 | /> 118 | } 121 | label="Use an existing Gist" 122 | /> 123 | 124 | {this.state.gistType === "new" && ( 125 | 136 | )} 137 | {this.state.gistType === "custom" && ( 138 |
145 | 152 | 155 |
156 | )} 157 | {this.state.createGistError && ( 158 |
{this.state.createGistError.message}
159 | )} 160 |
161 | ); 162 | } 163 | } 164 | 165 | const mapStateToProps = (state: RootState) => ({ 166 | accessToken: state.settings.accessToken, 167 | settingsGistId: state.settings.gistId, 168 | }); 169 | 170 | const mapDispatchToProps = { 171 | gistId: settingsActions.gistId, 172 | loadSnippets, 173 | }; 174 | 175 | export default connect(mapStateToProps, mapDispatchToProps)(SelectGist); 176 | -------------------------------------------------------------------------------- /src/editor/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { MemoryRouter, Route, Redirect } from "react-router-dom"; 4 | import { AnimatedSwitch } from "react-router-transition"; 5 | 6 | import { loadSnippets } from "../actions/snippets"; 7 | import { pages } from "../constants"; 8 | import { RootState } from "../reducers"; 9 | import { SettingsState } from "../reducers/settings"; 10 | import { Snippet, SnippetsState } from "../reducers/snippets"; 11 | import Login from "./Login"; 12 | import Main from "./Main"; 13 | import SelectGist from "./SelectGist"; 14 | import Settings from "./Settings"; 15 | import Welcome from "./Welcome"; 16 | 17 | import "typeface-roboto"; 18 | import "../main.css"; 19 | import "../settings.css"; 20 | 21 | const previousSnippetName = ( 22 | name: string, 23 | snippets: { [name: string]: Snippet } 24 | ) => { 25 | const sorted = Array.from( 26 | new Set(Object.keys(snippets).concat([name])) 27 | ).sort(); 28 | const index = sorted.indexOf(name); 29 | 30 | // Assumes there will be at least one remaining snippet 31 | if (index > 0) { 32 | return sorted[index - 1]; 33 | } 34 | 35 | // sorted[0] === name, select the next snippet 36 | return sorted[1]; 37 | }; 38 | 39 | const checkSelectedSnippet = ( 40 | selectedSnippetName: string | null, 41 | nextSnippets: { [name: string]: Snippet } | null, 42 | currentSnippets: { [name: string]: Snippet } | null 43 | ) => { 44 | // Select the first snippet on the first load 45 | if ( 46 | !selectedSnippetName && 47 | !currentSnippets && 48 | nextSnippets && 49 | Object.values(nextSnippets).length 50 | ) { 51 | return { selectedSnippetName: Object.keys(nextSnippets)[0] }; 52 | } 53 | 54 | if (!selectedSnippetName) { 55 | return null; 56 | } 57 | 58 | // If there are no snippets, remove the selection 59 | if ( 60 | !nextSnippets || 61 | !Object.values(nextSnippets).length || 62 | !Object.values(nextSnippets).find((s) => !s.deleted) 63 | ) { 64 | return { selectedSnippetName: null }; 65 | } 66 | 67 | // If the selected snippet was deleted or missing for some other reason, 68 | // select the previous snippet 69 | if ( 70 | (nextSnippets[selectedSnippetName] && 71 | nextSnippets[selectedSnippetName].deleted) || 72 | !nextSnippets[selectedSnippetName] 73 | ) { 74 | return { 75 | selectedSnippetName: previousSnippetName( 76 | selectedSnippetName, 77 | nextSnippets 78 | ), 79 | }; 80 | } 81 | 82 | if (!currentSnippets || !nextSnippets) { 83 | return null; 84 | } 85 | 86 | // If the selected snippet was renamed, select the new name 87 | // TODO this assumes a save just occurred, updating redux with the new 88 | // name. It's possible another snippet already existed with the new 89 | // name, but we can't tell the difference at this point. Should use 90 | // IDs for snippets instead of their name in the future. 91 | const newName = currentSnippets[selectedSnippetName].renamed; 92 | if (newName && nextSnippets[newName]) { 93 | return { 94 | selectedSnippetName: newName, 95 | }; 96 | } 97 | 98 | return null; 99 | }; 100 | 101 | type Props = { 102 | editorId: string; 103 | snippets: SnippetsState; 104 | settings: SettingsState; 105 | loadSnippets: () => void; 106 | runInInspectedWindow: (code: string) => void; 107 | }; 108 | 109 | type State = { 110 | selectedSnippetName: string | null; 111 | snippets: SnippetsState; 112 | }; 113 | 114 | class App extends React.Component { 115 | constructor(props: Props) { 116 | super(props); 117 | this.state = { 118 | selectedSnippetName: null, 119 | // Store snippets in state to access from getDerivedStateFromProps 120 | snippets: props.snippets, 121 | }; 122 | 123 | this.setSelectedSnippet = this.setSelectedSnippet.bind(this); 124 | } 125 | 126 | static getDerivedStateFromProps(props: Props, state: State) { 127 | if (state.snippets === props.snippets) { 128 | return null; 129 | } 130 | 131 | return { 132 | ...checkSelectedSnippet( 133 | state.selectedSnippetName, 134 | props.snippets && props.snippets.data, 135 | state.snippets && state.snippets.data 136 | ), 137 | snippets: props.snippets, 138 | }; 139 | } 140 | 141 | setSelectedSnippet(name: string) { 142 | this.setState({ selectedSnippetName: name }); 143 | } 144 | 145 | componentDidMount() { 146 | const { 147 | settings: { accessToken, gistId }, 148 | snippets: { data }, 149 | } = this.props; 150 | if (!data && accessToken && gistId) { 151 | this.props.loadSnippets(); 152 | } 153 | } 154 | 155 | componentDidUpdate() { 156 | const { 157 | settings: { accessToken, gistId }, 158 | snippets: { data, loading }, 159 | } = this.props; 160 | if (!data && !loading && accessToken && gistId) { 161 | this.props.loadSnippets(); 162 | } 163 | } 164 | 165 | // Choose a page based whether or not a Gist ID and token have 166 | // been entered 167 | choosePage() { 168 | const { accessToken, gistId } = this.props.settings; 169 | if (!accessToken && !gistId) { 170 | return pages.WELCOME; 171 | } else if (!accessToken) { 172 | return pages.LOGIN; 173 | } else if (!gistId) { 174 | return pages.SELECT_GIST; 175 | } else { 176 | return pages.MAIN; 177 | } 178 | } 179 | 180 | render() { 181 | return ( 182 | 183 | 189 | 190 | 191 | 192 | ( 195 |
202 | )} 203 | /> 204 | 205 | 206 | 207 | 208 | ); 209 | } 210 | } 211 | 212 | const mapStateToProps = (state: RootState) => ({ 213 | settings: state.settings, 214 | snippets: state.snippets, 215 | }); 216 | 217 | export default connect(mapStateToProps, { loadSnippets })(App); 218 | -------------------------------------------------------------------------------- /src/editor/reducers/snippets.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from "redux"; 2 | 3 | import { DeepPartial } from "../../types"; 4 | import * as settingsActions from "../actions/settings"; 5 | import { 6 | CREATE_SNIPPET, 7 | RENAME_SNIPPET, 8 | UPDATE_SNIPPET, 9 | DELETE_SNIPPET, 10 | LOADING_SNIPPETS, 11 | LOADED_SNIPPETS, 12 | SAVING_SNIPPETS, 13 | SAVED_SNIPPETS, 14 | LOADED_LEGACY_SNIPPETS, 15 | } from "../actions/snippets"; 16 | import { mergeDeep as merge } from "../util/deep-merge"; 17 | 18 | const nextUniqueName = ( 19 | name: string, 20 | existingNames: string[], 21 | append = 0 22 | ): string => 23 | existingNames.includes(name + (append ? "-" + append : "")) 24 | ? nextUniqueName(name, existingNames, append + 1) 25 | : name + (append ? "-" + append : ""); 26 | 27 | export type Snippet = { 28 | deleted: boolean; 29 | renamed: string | false; 30 | /** 31 | * Used by each panel's editor to know when to update. Editors ignore their 32 | * own updates, but accept updates from other editors. 33 | */ 34 | lastUpdatedBy?: string; 35 | content: { 36 | local: string; 37 | remote: string | false; 38 | }; 39 | }; 40 | 41 | export type SnippetsState = { 42 | loading: boolean; 43 | saving: boolean; 44 | error: Error | null; 45 | data: { [name: string]: Snippet } | null; 46 | }; 47 | 48 | const defaultState: SnippetsState = { 49 | loading: false, 50 | saving: false, 51 | error: null, 52 | data: null, 53 | }; 54 | 55 | const mergeState = (oldState: S) => ( 56 | newState: DeepPartial 57 | ): S => merge({} as S, oldState, newState as any); 58 | 59 | const snippets = (state = defaultState, action: AnyAction): SnippetsState => { 60 | const update = mergeState(state); 61 | switch (action.type) { 62 | case CREATE_SNIPPET: 63 | return !state.loading && state.data 64 | ? update({ 65 | data: { 66 | [nextUniqueName("untitled", Object.keys(state.data))]: { 67 | deleted: false, 68 | renamed: false, 69 | content: { 70 | local: "", 71 | remote: false, 72 | }, 73 | }, 74 | }, 75 | }) 76 | : state; 77 | case RENAME_SNIPPET: 78 | return !state.loading && 79 | state.data && 80 | state.data[action.oldName] && 81 | action.newName 82 | ? update({ 83 | data: { 84 | [action.oldName]: { 85 | renamed: 86 | action.newName === action.oldName ? false : action.newName, 87 | }, 88 | }, 89 | }) 90 | : state; 91 | case UPDATE_SNIPPET: 92 | return !state.loading && state.data 93 | ? update({ 94 | data: { 95 | [action.name]: { 96 | lastUpdatedBy: action.editorId, 97 | content: { 98 | local: action.newBody, 99 | }, 100 | }, 101 | }, 102 | }) 103 | : state; 104 | case DELETE_SNIPPET: 105 | if (state.loading || !state.data) { 106 | return state; 107 | } 108 | 109 | const snippet = state.data[action.name]; 110 | if (!snippet) { 111 | return state; 112 | } 113 | 114 | if (snippet.content.remote === false) { 115 | const snippets = { ...state.data }; 116 | delete snippets[action.name]; 117 | return { ...state, data: snippets }; 118 | } 119 | 120 | return update({ 121 | data: { 122 | [action.name]: { 123 | deleted: true, 124 | }, 125 | }, 126 | }); 127 | case LOADING_SNIPPETS: 128 | return update({ loading: true, error: null }); 129 | case LOADED_SNIPPETS: 130 | return action.error 131 | ? { loading: false, error: action.error, saving: false, data: null } 132 | : { 133 | saving: state.saving, 134 | error: null, 135 | loading: false, 136 | data: Object.entries(action.snippets).reduce( 137 | (snippets, [name, { body }]) => { 138 | snippets[name] = { 139 | deleted: false, 140 | renamed: false, 141 | content: { 142 | local: body, 143 | remote: body, 144 | }, 145 | }; 146 | return snippets; 147 | }, 148 | {} as { [name: string]: Snippet } 149 | ), 150 | }; 151 | case SAVING_SNIPPETS: 152 | return update({ saving: true, error: null }); 153 | case SAVED_SNIPPETS: 154 | return action.error 155 | ? { 156 | loading: false, 157 | saving: false, 158 | error: action.error, 159 | data: state.data, 160 | } 161 | : { 162 | loading: false, 163 | saving: false, 164 | error: null, 165 | data: Object.entries(state.data || {}).reduce( 166 | (accum, [name, snippet]) => { 167 | if (!snippet.deleted) { 168 | accum[snippet.renamed ? snippet.renamed : name] = { 169 | renamed: false, 170 | deleted: false, 171 | content: { 172 | local: snippet.content.local, 173 | remote: snippet.content.local, 174 | }, 175 | }; 176 | } 177 | return accum; 178 | }, 179 | {} as { [name: string]: Snippet } 180 | ), 181 | }; 182 | case LOADED_LEGACY_SNIPPETS: 183 | return action.error 184 | ? { 185 | loading: false, 186 | saving: false, 187 | error: action.error, 188 | data: state.data, 189 | } 190 | : { 191 | loading: false, 192 | saving: false, 193 | error: null, 194 | data: Object.entries(action.snippets).reduce( 195 | (snippets, [name, body]) => { 196 | snippets[name] = { 197 | renamed: false, 198 | deleted: false, 199 | content: { 200 | local: body, 201 | remote: snippets[name] && snippets[name].content.remote, 202 | }, 203 | }; 204 | return snippets; 205 | }, 206 | state.data || {} 207 | ), 208 | }; 209 | case settingsActions.types.gistId: 210 | case settingsActions.types.accessToken: 211 | return update({ error: null, data: null }); 212 | default: 213 | return state; 214 | } 215 | }; 216 | 217 | export default snippets; 218 | -------------------------------------------------------------------------------- /docs/async-store-workaround.md: -------------------------------------------------------------------------------- 1 | # The Observed Problem 2 | 3 | The cursor could jump around in the text editors, especially when typing quickly or inserting a suggestion from the text editor (aka Ace snippet). 4 | 5 | **TLDR: Ace works when store updates are synchronous, but webext-redux causes updates to be asynchronous** 6 | 7 | What the heck does that mean? Well, I'm glad you asked! 8 | 9 | When events are synchronous, they are processed by Ace and Redux in the order they occur. When events are asynchronous, events might get processed out of order. 10 | 11 | Let's look at the difference between an synchronous and asynchronous update: 12 | 13 | # Synchronous Update 14 | 15 | (react + react-redux + react-ace) 16 | 17 | 1. Browser emits keyboard event in DOM 18 | 2. Ace updates internal state 19 | 3. Ace emits 'change' 20 | 4. dispatch `updateEditorValue` on store 21 | 5. update store state via reducer 22 | 6. react-redux re-renders editor component 23 | 7. editor component passes value to react-ace 24 | 25 | Ace updated in step 2, so the value prop and internal value are now equal 26 | 27 | If another key is pressed before this finishes, the browser holds the event in a queue before firing it because JavaScript is not designed to be interrupted while it's running. 28 | 29 | # Asynchronous Update 30 | 31 | (react + react-redux + react-ace + webext-redux) 32 | 33 | Events basically follow the same steps here, but `webext-redux` makes the processing asynchronous, because it has to pass messages through the runtime API to get them to the redux store and back to the react app. 34 | 35 | ![diagram of Redux updates with webext-redux](https://camo.githubusercontent.com/1eb2b13d733b8ade35770c439473bb1cf5bd3ef5/68747470733a2f2f692e696d6775722e636f6d2f33454e554d6a302e706e67) 36 | 37 | Source: [webext-redux wiki](https://github.com/tshaddix/webext-redux/wiki/Introduction#webext-redux) 38 | 39 | Events start in a devtools panel, content script, or popup. In Snippets, this is a devtools panel. 40 | 41 | 1. Browser emits keyboard event in DOM 42 | 2. Ace updates internal state 43 | 3. Ace emits 'change' 44 | 4. dispatch `updateEditorValue` on store 45 | 5. dispatch is sent to background page using the runtime API 46 | 47 | Between step 5 and 9, the devtools panel is able to process new DOM events. 48 | 49 | _In the background page_ 50 | 51 | 6. Receive dispatch message from devtools panel 52 | 7. update store state via reducer 53 | 8. webext-redux broadcasts the new state 54 | 55 | _Back in the devtools panel_ 56 | 57 | 9. Receive state update message from runtime API 58 | 10. react-redux re-renders editor component 59 | 11. editor component passes value to Ace 60 | 61 | If no other events have come in since step 5, the prop passed to react-ace and Ace's value are now equal, and there's no problem. 62 | 63 | However, if Ace has already received other keyboard events, the store will have an old value. (The new value is still propagating through the same process). Ace gets the stale value, even though Ace already had the newest value! 64 | 65 | When updates are synchronous, Ace can only be one event ahead of the store. 66 | 67 | When updates are asynchronous, and Ace might get more than one event ahead of the store, which causes the events to be "replayed" as Ace receives them from the store. It causes all kinds of desyc bugs, the most obvious being the cursor can jump around. 68 | 69 | # Potential Solutions 70 | 71 | ### Debounce `onChange` 72 | 73 | Difficulty to implement: very easy 74 | Pros: easy 75 | Cons: won't totally prevent the problem 76 | 77 | If we debounce the dispatching of onChange, we can accumulate consecutive events. However this introduces a delay in updating the store, and it is not bulletproof; new events might still come in when we finally decide to dispatch, regardless of how long we wait. 78 | 79 | ### Stop Ace from processing new DOM events until the store has caught up 80 | 81 | Difficulty to implement: hard 82 | Pros: should allow syncing editors between pages 83 | Cons: unknown 84 | 85 | Basically implement our own event queue. We need to know when an event has fully propagated back to Ace before firing the next event. 86 | 87 | ### Use an uncontrolled editor 88 | 89 | Difficulty to implement: easy\* 90 | Pros: easy\* 91 | Cons: \*unless we have multiple editors 92 | 93 | This is easy to implement, but comes with a problem: if we have editors in different devtool panels, we need some way to sync them up. This could be worked around by using a single editor. Otherwise we need to identify which editor caused the updates, and update the value of all _other_ editors. 94 | 95 | ### Fire 'versioned' state updates 96 | 97 | Difficulty to implement: unknown (but probably really hard) 98 | Pros: solves our original problem (not much else) 99 | Cons: unknown 100 | 101 | We can assign each dispatch a generation version. As values come back from the store, we can compare them to Ace's value, and ignore generations that are behind what Ace has. Tracking the version will be... interesting, to say the least. Especially across devtool panels. We can't keep it in the redux store, because it has to be based on Ace's internal state. 102 | 103 | ### Synchronously dispatch events in webext-redux 104 | 105 | Difficulty to implement: really hard 106 | Pros: redux should be synchronous anyway 107 | Cons: see difficulty 108 | 109 | Each devtools panel store would run reducers itself and synchronously re-render. However this means the store in the background page is no longer the single-source of truth. It would require some kind of conflict resolution to keep the panels and the background store in sync... maybe we could use a Git-like system (panel stores are individual repos and background store is the remote)? That seems like overkill though. 110 | 111 | Technically, Redux is normally synchronous and webext-redux breaks that part of Redux. I don't know how many applications are affected by it though. 112 | 113 | ### Remove Ace's internal state 114 | 115 | Difficulty to implement: really hard 116 | Pros: Who doesn't want a pure(1) Ace implementation? 117 | Cons: see difficulty 118 | 119 | The only reason this is a problem is because there are two sources of truth: redux and Ace. If Ace only uses the provided value, instead of using internal state, we can avoid Ace getting ahead of Redux. However, this _could_ cause a noticeable lag while typing. I'm not sure though. 120 | 121 | It's "really hard" because you would need to rip out ALL of Ace's internal state and move it to the redux store. Technically possible, but Ace is not designed to allow that. 122 | 123 | 1: "Pure" here means "no side effects" (including internal state) 124 | 125 | ### Remove webext-redux from Snippets 126 | 127 | Difficulty to implement: hard 128 | Pros: solves our original problem (not much else) 129 | Cons: major architecture change 130 | 131 | We could use a different system to sync editors, remove syncing entirely, or remove editors from the devtools and use a single editor. 132 | 133 | # Chosen Solution 134 | 135 | ### Use an uncontrolled editor 136 | 137 | I chose this option because it works with the current design (where each devtools panel has its own editor), and it doesn't require modifying any libraries. 138 | 139 | I added a new value to each snippet in the Redux store, `lastUpdatedBy`. As state updates come in from Redux, each editor will ignore updates that were dispatched from themselves, while updating their state to match updates from other editors. 140 | -------------------------------------------------------------------------------- /src/mode-javascript-eslint/worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // In Ace, this code sets up globals in web workers. doesn't exist as a 4 | // stand-alone module in ace-builds, copied from (with minor changes) 5 | // https://github.com/ajaxorg/ace/blob/3ebb0c34dc023aff3b6a951dd116a702510369fe/lib/ace/worker/worker.js 6 | 7 | "no use strict"; 8 | !(function (window) { 9 | if (typeof window.window != "undefined" && window.document) return; 10 | if (window.require && window.define) return; 11 | 12 | if (!window.console) { 13 | window.console = function () { 14 | var msgs = Array.prototype.slice.call(arguments, 0); 15 | postMessage({ type: "log", data: msgs }); 16 | }; 17 | window.console.error = window.console.warn = window.console.log = window.console.trace = 18 | window.console; 19 | } 20 | window.window = window; 21 | window.ace = window; 22 | 23 | window.onerror = function (message, file, line, col, err) { 24 | postMessage({ 25 | type: "error", 26 | data: { 27 | message: message, 28 | data: err.data, 29 | file: file, 30 | line: line, 31 | col: col, 32 | stack: err.stack, 33 | }, 34 | }); 35 | }; 36 | 37 | window.normalizeModule = function (parentId, moduleName) { 38 | // normalize plugin requires 39 | if (moduleName.indexOf("!") !== -1) { 40 | var chunks = moduleName.split("!"); 41 | return ( 42 | window.normalizeModule(parentId, chunks[0]) + 43 | "!" + 44 | window.normalizeModule(parentId, chunks[1]) 45 | ); 46 | } 47 | // normalize relative requires 48 | if (moduleName.charAt(0) == ".") { 49 | var base = parentId.split("/").slice(0, -1).join("/"); 50 | moduleName = (base ? base + "/" : "") + moduleName; 51 | 52 | while (moduleName.indexOf(".") !== -1 && previous != moduleName) { 53 | var previous = moduleName; 54 | moduleName = moduleName 55 | .replace(/^\.\//, "") 56 | .replace(/\/\.\//, "/") 57 | .replace(/[^\/]+\/\.\.\//, ""); 58 | } 59 | } 60 | 61 | return moduleName; 62 | }; 63 | 64 | window.require = function require(parentId, id) { 65 | if (!id) { 66 | id = parentId; 67 | parentId = null; 68 | } 69 | if (!id.charAt) 70 | throw new Error( 71 | "worker.js require() accepts only (parentId, id) as arguments" 72 | ); 73 | 74 | id = window.normalizeModule(parentId, id); 75 | 76 | var module = window.require.modules[id]; 77 | if (module) { 78 | if (!module.initialized) { 79 | module.initialized = true; 80 | module.exports = module.factory().exports; 81 | } 82 | return module.exports; 83 | } 84 | 85 | if (!window.require.tlns) return console.log("unable to load " + id); 86 | 87 | var path = resolveModuleId(id, window.require.tlns); 88 | if (path.slice(-3) != ".js") path += ".js"; 89 | 90 | window.require.id = id; 91 | window.require.modules[id] = {}; // prevent infinite loop on broken modules 92 | importScripts(path); 93 | return window.require(parentId, id); 94 | }; 95 | function resolveModuleId(id, paths) { 96 | var testPath = id, 97 | tail = ""; 98 | while (testPath) { 99 | var alias = paths[testPath]; 100 | if (typeof alias == "string") { 101 | return alias + tail; 102 | } else if (alias) { 103 | return ( 104 | alias.location.replace(/\/*$/, "/") + 105 | (tail || alias.main || alias.name) 106 | ); 107 | } else if (alias === false) { 108 | return ""; 109 | } 110 | var i = testPath.lastIndexOf("/"); 111 | if (i === -1) break; 112 | tail = testPath.substr(i) + tail; 113 | testPath = testPath.slice(0, i); 114 | } 115 | return id; 116 | } 117 | window.require.modules = {}; 118 | window.require.tlns = {}; 119 | 120 | window.define = function (id, deps, factory) { 121 | if (arguments.length == 2) { 122 | factory = deps; 123 | if (typeof id != "string") { 124 | deps = id; 125 | id = window.require.id; 126 | } 127 | } else if (arguments.length == 1) { 128 | factory = id; 129 | deps = []; 130 | id = window.require.id; 131 | } 132 | 133 | if (typeof factory != "function") { 134 | window.require.modules[id] = { 135 | exports: factory, 136 | initialized: true, 137 | }; 138 | return; 139 | } 140 | 141 | if (!deps.length) 142 | // If there is no dependencies, we inject "require", "exports" and 143 | // "module" as dependencies, to provide CommonJS compatibility. 144 | deps = ["require", "exports", "module"]; 145 | 146 | var req = function (childId) { 147 | return window.require(id, childId); 148 | }; 149 | 150 | window.require.modules[id] = { 151 | exports: {}, 152 | factory: function () { 153 | var module = this; 154 | var returnExports = factory.apply( 155 | this, 156 | deps.slice(0, factory.length).map(function (dep) { 157 | switch (dep) { 158 | // Because "require", "exports" and "module" aren't actual 159 | // dependencies, we must handle them seperately. 160 | case "require": 161 | return req; 162 | case "exports": 163 | return module.exports; 164 | case "module": 165 | return module; 166 | // But for all other dependencies, we can just go ahead and 167 | // require them. 168 | default: 169 | return req(dep); 170 | } 171 | }) 172 | ); 173 | if (returnExports) module.exports = returnExports; 174 | return module; 175 | }, 176 | }; 177 | }; 178 | window.define.amd = {}; 179 | window.require.tlns = {}; 180 | window.initBaseUrls = function initBaseUrls(topLevelNamespaces) { 181 | for (var i in topLevelNamespaces) 182 | window.require.tlns[i] = topLevelNamespaces[i]; 183 | }; 184 | 185 | window.initSender = function initSender() { 186 | var EventEmitter = window.require("ace/lib/event_emitter").EventEmitter; 187 | var oop = window.require("ace/lib/oop"); 188 | 189 | var Sender = function () {}; 190 | 191 | (function () { 192 | oop.implement(this, EventEmitter); 193 | 194 | this.callback = function (data, callbackId) { 195 | postMessage({ 196 | type: "call", 197 | id: callbackId, 198 | data: data, 199 | }); 200 | }; 201 | 202 | this.emit = function (name, data) { 203 | postMessage({ 204 | type: "event", 205 | name: name, 206 | data: data, 207 | }); 208 | }; 209 | }.call(Sender.prototype)); 210 | 211 | return new Sender(); 212 | }; 213 | 214 | var main = (window.main = null); 215 | var sender = (window.sender = null); 216 | 217 | window.onmessage = function (e) { 218 | var msg = e.data; 219 | if (msg.event && sender) { 220 | sender._signal(msg.event, msg.data); 221 | } else if (msg.command) { 222 | if (main[msg.command]) main[msg.command].apply(main, msg.args); 223 | else if (window[msg.command]) window[msg.command].apply(window, msg.args); 224 | else throw new Error("Unknown command:" + msg.command); 225 | } else if (msg.init) { 226 | window.initBaseUrls(msg.tlns); 227 | sender = window.sender = window.initSender(); 228 | var clazz = window.require(msg.module)[msg.classname]; 229 | main = window.main = new clazz(sender); 230 | } 231 | }; 232 | })(self); 233 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const restrictedGlobals = require("confusing-browser-globals"); 2 | 3 | module.exports = { 4 | root: true, 5 | 6 | parser: "@typescript-eslint/parser", 7 | 8 | parserOptions: { 9 | sourceType: "script", 10 | }, 11 | 12 | plugins: ["@typescript-eslint", "import", "react", "react-hooks"], 13 | 14 | globals: { 15 | chrome: true, 16 | }, 17 | 18 | settings: { 19 | react: { 20 | version: "detect", 21 | }, 22 | }, 23 | 24 | // NOTE: When adding rules here, you need to make sure they are compatible with 25 | // `typescript-eslint`, as some rules such as `no-array-constructor` aren't compatible. 26 | rules: { 27 | "dot-location": ["warn", "property"], 28 | eqeqeq: ["warn", "smart"], 29 | "new-parens": "warn", 30 | "no-caller": "warn", 31 | "no-cond-assign": ["warn", "except-parens"], 32 | "no-const-assign": "warn", 33 | "no-control-regex": "warn", 34 | "no-delete-var": "warn", 35 | "no-dupe-args": "warn", 36 | "no-dupe-keys": "warn", 37 | "no-duplicate-case": "warn", 38 | "no-empty-character-class": "warn", 39 | "no-empty-pattern": "warn", 40 | "no-eval": "warn", 41 | "no-ex-assign": "warn", 42 | "no-extend-native": "warn", 43 | "no-extra-bind": "warn", 44 | "no-extra-label": "warn", 45 | "no-fallthrough": "warn", 46 | "no-func-assign": "warn", 47 | "no-implied-eval": "warn", 48 | "no-invalid-regexp": "warn", 49 | "no-iterator": "warn", 50 | "no-label-var": "warn", 51 | "no-labels": ["warn", { allowLoop: true, allowSwitch: false }], 52 | "no-lone-blocks": "warn", 53 | "no-loop-func": "warn", 54 | "no-mixed-operators": [ 55 | "warn", 56 | { 57 | groups: [ 58 | ["&", "|", "^", "~", "<<", ">>", ">>>"], 59 | ["==", "!=", "===", "!==", ">", ">=", "<", "<="], 60 | ["&&", "||"], 61 | ["in", "instanceof"], 62 | ], 63 | allowSamePrecedence: false, 64 | }, 65 | ], 66 | "no-multi-str": "warn", 67 | "no-native-reassign": "warn", 68 | "no-negated-in-lhs": "warn", 69 | "no-new-func": "warn", 70 | "no-new-object": "warn", 71 | "no-new-symbol": "warn", 72 | "no-new-wrappers": "warn", 73 | "no-obj-calls": "warn", 74 | "no-octal": "warn", 75 | "no-octal-escape": "warn", 76 | "no-redeclare": "warn", 77 | "no-regex-spaces": "warn", 78 | "no-restricted-syntax": ["warn", "WithStatement"], 79 | "no-script-url": "warn", 80 | "no-self-assign": "warn", 81 | "no-self-compare": "warn", 82 | "no-sequences": "warn", 83 | "no-shadow-restricted-names": "warn", 84 | "no-sparse-arrays": "warn", 85 | "no-template-curly-in-string": "warn", 86 | "no-this-before-super": "warn", 87 | "no-throw-literal": "warn", 88 | "no-restricted-globals": ["error"].concat(restrictedGlobals), 89 | "no-unreachable": "warn", 90 | "no-unused-labels": "warn", 91 | "no-useless-computed-key": "warn", 92 | "no-useless-concat": "warn", 93 | "no-useless-escape": "warn", 94 | "no-useless-rename": [ 95 | "warn", 96 | { 97 | ignoreDestructuring: false, 98 | ignoreImport: false, 99 | ignoreExport: false, 100 | }, 101 | ], 102 | "no-with": "warn", 103 | "no-whitespace-before-property": "warn", 104 | "require-yield": "warn", 105 | "rest-spread-spacing": ["warn", "never"], 106 | "unicode-bom": ["warn", "never"], 107 | "use-isnan": "warn", 108 | "valid-typeof": "warn", 109 | "no-restricted-properties": [ 110 | "error", 111 | { 112 | object: "require", 113 | property: "ensure", 114 | message: "Please use import() instead", 115 | }, 116 | { 117 | object: "System", 118 | property: "import", 119 | message: "Please use import() instead", 120 | }, 121 | ], 122 | "getter-return": "warn", 123 | "no-debugger": "warn", 124 | 125 | // IMPORT 126 | 127 | "import/first": "error", 128 | "import/no-amd": "error", 129 | "import/no-webpack-loader-syntax": "error", 130 | "import/no-extraneous-dependencies": "warn", 131 | "import/order": [ 132 | "warn", 133 | { 134 | groups: ["builtin", "external"], 135 | pathGroups: [ 136 | { 137 | pattern: "@/**", 138 | group: "internal", 139 | }, 140 | ], 141 | "newlines-between": "always", 142 | pathGroupsExcludedImportTypes: ["builtin"], 143 | alphabetize: { order: "asc" }, 144 | }, 145 | ], 146 | 147 | // REACT 148 | 149 | "react/forbid-foreign-prop-types": ["warn", { allowInPropTypes: true }], 150 | "react/jsx-no-comment-textnodes": "warn", 151 | "react/jsx-no-duplicate-props": "warn", 152 | "react/jsx-no-target-blank": "warn", 153 | "react/jsx-no-undef": "error", 154 | "react/jsx-pascal-case": [ 155 | "warn", 156 | { 157 | allowAllCaps: true, 158 | ignore: [], 159 | }, 160 | ], 161 | "react/jsx-uses-react": "warn", 162 | "react/jsx-uses-vars": "warn", 163 | "react/no-danger-with-children": "warn", 164 | "react/no-direct-mutation-state": "warn", 165 | "react/no-is-mounted": "warn", 166 | "react/no-typos": "error", 167 | "react/require-render-return": "error", 168 | "react/style-prop-object": "warn", 169 | "react/jsx-boolean-value": ["warn", "never"], 170 | 171 | // REACT-HOOKS 172 | 173 | "react-hooks/exhaustive-deps": "warn", 174 | "react-hooks/rules-of-hooks": "error", 175 | 176 | // TYPESCRIPT 177 | 178 | "@typescript-eslint/consistent-type-assertions": "warn", 179 | "@typescript-eslint/no-array-constructor": "warn", 180 | "@typescript-eslint/no-use-before-define": [ 181 | "warn", 182 | { 183 | functions: false, 184 | classes: false, 185 | variables: false, 186 | typedefs: false, 187 | }, 188 | ], 189 | "@typescript-eslint/no-unused-expressions": [ 190 | "error", 191 | { 192 | allowShortCircuit: true, 193 | allowTernary: true, 194 | allowTaggedTemplates: true, 195 | }, 196 | ], 197 | "@typescript-eslint/no-unused-vars": [ 198 | "warn", 199 | { 200 | args: "none", 201 | ignoreRestSiblings: true, 202 | }, 203 | ], 204 | "@typescript-eslint/no-useless-constructor": "warn", 205 | "@typescript-eslint/no-for-in-array": "warn", 206 | "@typescript-eslint/no-namespace": "warn", 207 | "@typescript-eslint/prefer-as-const": "warn", 208 | "@typescript-eslint/no-invalid-this": "error", 209 | }, 210 | 211 | overrides: [ 212 | { 213 | // Files inside of `src` 214 | files: ["src/**/*"], 215 | parserOptions: { 216 | project: "./tsconfig.json", 217 | tsconfigRootDir: __dirname, 218 | ecmaVersion: 2018, 219 | sourceType: "module", 220 | ecmaFeatures: { 221 | jsx: true, 222 | }, 223 | warnOnUnsupportedTypeScriptVersion: true, 224 | }, 225 | rules: { 226 | strict: ["warn", "never"], 227 | // These rules won't work outside of `src` because they require type 228 | // information, which is only generated for `src`. 229 | "@typescript-eslint/await-thenable": "warn", 230 | "@typescript-eslint/no-unnecessary-type-assertion": "warn", 231 | }, 232 | }, 233 | { 234 | files: ["*.d.ts"], 235 | rules: { 236 | // Doesn't seem to understand definition files 237 | "import/order": "off", 238 | }, 239 | }, 240 | { 241 | files: ["src/mode-javascript-eslint/**/*"], 242 | rules: { 243 | // Only sees the sub-package.json, and thinks ace-builds is missing 244 | // when it is actually resolved from the top-level package.json 245 | "import/no-extraneous-dependencies": "off", 246 | }, 247 | }, 248 | ], 249 | }; 250 | -------------------------------------------------------------------------------- /src/editor/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { Redirect, RouteComponentProps } from "react-router-dom"; 4 | 5 | import logo from "../../../images/logo-transparent.png"; 6 | import { actions as settingsActions } from "../actions/settings"; 7 | import { 8 | createSnippet, 9 | renameSnippet, 10 | updateSnippet, 11 | deleteSnippet, 12 | saveSnippets, 13 | loadSnippets, 14 | } from "../actions/snippets"; 15 | import { pages, SNIPPETS_ISSUES_URL } from "../constants"; 16 | import { RootState } from "../reducers"; 17 | import { SettingsState } from "../reducers/settings"; 18 | import { Snippet, SnippetsState } from "../reducers/snippets"; 19 | import Editor from "./Editor"; 20 | import ErrorPage from "./ErrorPage"; 21 | import Loading from "./Loading"; 22 | import Sidepane from "./Sidepane"; 23 | 24 | enum SaveStatus { 25 | Saving = "SAVING", 26 | Saved = "SAVED", 27 | Unsaved = "UNSAVED", 28 | } 29 | 30 | type Props = { 31 | editorId: string; 32 | snippets: { 33 | loading: boolean; 34 | error: Error | null; 35 | data: { [name: string]: Snippet } | null; 36 | }; 37 | selectedSnippetName: string | null; 38 | saveStatus: SaveStatus; 39 | settings: SettingsState; 40 | runInInspectedWindow: (code: string) => void; 41 | saveSnippets: () => void; 42 | loadSnippets: () => void; 43 | setSelectedSnippet: (name: string) => void; 44 | updateSnippet: (name: string, value: string, editorId: string) => void; 45 | renameSnippet: (oldName: string, newName: string) => void; 46 | createSnippet: (name: string) => void; 47 | deleteSnippet: (name: string) => void; 48 | accessToken: (token: string | false) => void; 49 | gistId: (gistId: string | false) => void; 50 | }; 51 | 52 | class Main extends React.Component { 53 | constructor(props: Props) { 54 | super(props as any); 55 | 56 | this.handleKeyPress = this.handleKeyPress.bind(this); 57 | this.handleEditorChange = this.handleEditorChange.bind(this); 58 | this.runSnippet = this.runSnippet.bind(this); 59 | } 60 | 61 | runSnippet(snippetBody: string) { 62 | const code = ` 63 | try { 64 | ${snippetBody} 65 | } catch (e) { 66 | console.error(e) 67 | } 68 | `; 69 | 70 | this.props.runInInspectedWindow(code); 71 | } 72 | 73 | handleKeyPress(event: React.KeyboardEvent) { 74 | if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) { 75 | const { selectedSnippetName } = this.props; 76 | const { snippets } = this.props; 77 | const snippet = 78 | selectedSnippetName && 79 | snippets.data && 80 | snippets.data[selectedSnippetName]; 81 | if (snippet) { 82 | this.runSnippet(snippet.content.local); 83 | } 84 | } 85 | } 86 | 87 | handleEditorChange(newValue: string) { 88 | if (!this.props.selectedSnippetName) { 89 | return; 90 | } 91 | 92 | this.props.updateSnippet( 93 | this.props.selectedSnippetName, 94 | newValue, 95 | this.props.editorId 96 | ); 97 | } 98 | 99 | renderEditor(snippets: { [name: string]: Snippet }) { 100 | const { selectedSnippetName: snippetId } = this.props; 101 | const snippet = snippetId && snippets[snippetId]; 102 | 103 | if (snippetId && snippet) { 104 | return ( 105 | 112 | ); 113 | } else { 114 | return ( 115 |
116 | 117 |

Nothing Selected

118 |
119 | ); 120 | } 121 | } 122 | 123 | renderSaveMessage() { 124 | const { 125 | snippets: { error }, 126 | } = this.props; 127 | if (error) { 128 | return ( 129 | 130 | {(error as any).context 131 | ? "Failed to " + (error as any).context 132 | : "Error"} 133 | : {error.message} -- click to retry 134 | 135 | ); 136 | } 137 | switch (this.props.saveStatus) { 138 | case SaveStatus.Saved: 139 | return Saved; 140 | case SaveStatus.Saving: 141 | return Saving...; 142 | case SaveStatus.Unsaved: 143 | return ( 144 | 145 | You have unsaved changes 146 | 147 | ); 148 | default: 149 | throw new Error("Unexpected save status:" + this.props.saveStatus); 150 | } 151 | } 152 | 153 | renderMain(snippets: { [name: string]: Snippet }) { 154 | return ( 155 |
156 | this.props.history.push(pages.SETTINGS)} 165 | /> 166 |
167 |
{this.renderSaveMessage()}
168 | {this.renderEditor(snippets)} 169 |
170 |
171 | ); 172 | } 173 | 174 | renderError(error: Error) { 175 | switch ((error as any).status) { 176 | case 401: // Unauthorized 177 | return ( 178 | { 183 | this.props.accessToken(false); 184 | this.props.history.push(pages.LOGIN); 185 | }} 186 | /> 187 | ); 188 | 189 | case 404: // Not found 190 | return ( 191 | { 196 | this.props.gistId(false); 197 | this.props.history.push(pages.SELECT_GIST); 198 | }} 199 | /> 200 | ); 201 | 202 | default: 203 | return ( 204 | this.props.history.push(pages.SETTINGS)} 210 | link={SNIPPETS_ISSUES_URL} 211 | /> 212 | ); 213 | } 214 | } 215 | 216 | render() { 217 | const { snippets: snippetsState } = this.props; 218 | if (snippetsState.loading) { 219 | return ; 220 | } else if (snippetsState.error && !snippetsState.data) { 221 | return this.renderError(snippetsState.error); 222 | } else if (!snippetsState.data) { 223 | if (!this.props.settings.accessToken) { 224 | return ; 225 | } 226 | if (!this.props.settings.gistId) { 227 | return ; 228 | } 229 | return ( 230 | 236 | ); 237 | } else { 238 | return this.renderMain(snippetsState.data); 239 | } 240 | } 241 | } 242 | 243 | const isSnippetUnsaved = (snippet: Snippet) => { 244 | return ( 245 | snippet.content.local !== snippet.content.remote || 246 | snippet.deleted || 247 | snippet.renamed 248 | ); 249 | }; 250 | 251 | const getSaveStatus = (state: SnippetsState) => { 252 | if (state.saving) { 253 | return SaveStatus.Saving; 254 | } 255 | 256 | if (!state.data) { 257 | return SaveStatus.Saved; 258 | } 259 | 260 | const unsaved = Object.entries(state.data).find(([name, snippet]) => 261 | isSnippetUnsaved(snippet) 262 | ); 263 | if (unsaved) { 264 | return SaveStatus.Unsaved; 265 | } 266 | 267 | return SaveStatus.Saved; 268 | }; 269 | 270 | const mapStateToProps = (state: RootState) => ({ 271 | settings: state.settings, 272 | snippets: { 273 | loading: state.snippets.loading, 274 | error: state.snippets.error, 275 | data: state.snippets.data 276 | ? Object.entries(state.snippets.data).reduce( 277 | (snippets, [name, snippet]) => { 278 | if (!snippet.deleted) { 279 | snippets[name] = snippet; 280 | } 281 | return snippets; 282 | }, 283 | {} as { [name: string]: Snippet } 284 | ) 285 | : state.snippets.data, 286 | }, 287 | saveStatus: getSaveStatus(state.snippets), 288 | }); 289 | 290 | const mapDispatchToProps = { 291 | createSnippet, 292 | renameSnippet, 293 | updateSnippet, 294 | deleteSnippet, 295 | saveSnippets, 296 | loadSnippets, 297 | accessToken: settingsActions.accessToken, 298 | gistId: settingsActions.gistId, 299 | }; 300 | 301 | export default connect(mapStateToProps, mapDispatchToProps)(Main); 302 | -------------------------------------------------------------------------------- /src/editor/components/Settings.tsx: -------------------------------------------------------------------------------- 1 | import AppBar from "@material-ui/core/AppBar"; 2 | import Button from "@material-ui/core/Button"; 3 | import Divider from "@material-ui/core/Divider"; 4 | import IconButton from "@material-ui/core/IconButton"; 5 | import List from "@material-ui/core/List"; 6 | import ListItem from "@material-ui/core/ListItem"; 7 | import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction"; 8 | import ListItemText from "@material-ui/core/ListItemText"; 9 | import MenuItem from "@material-ui/core/MenuItem"; 10 | import Select from "@material-ui/core/Select"; 11 | import Switch from "@material-ui/core/Switch"; 12 | import TextField from "@material-ui/core/TextField"; 13 | import Toolbar from "@material-ui/core/Toolbar"; 14 | import ArrowBackIcon from "@material-ui/icons/ArrowBack"; 15 | import PublishIcon from "@material-ui/icons/Publish"; 16 | import RefreshIcon from "@material-ui/icons/Refresh"; 17 | import VisibilityIcon from "@material-ui/icons/Visibility"; 18 | import React from "react"; 19 | import { connect } from "react-redux"; 20 | import { RouteComponentProps } from "react-router-dom"; 21 | 22 | import logo from "../../../images/logo-transparent.png"; 23 | import { actions } from "../actions/settings"; 24 | import { loadSnippets, loadLegacySnippets } from "../actions/snippets"; 25 | import { pages } from "../constants"; 26 | import { RootState } from "../reducers"; 27 | import { SettingsState } from "../reducers/settings"; 28 | import SettingsGroup from "./SettingsGroup"; 29 | 30 | // TODO Move these to ../constants 31 | const themes = { 32 | github: "Github", 33 | tomorrow_night: "Tomorrow Night", 34 | }; 35 | 36 | const tabTypes = { 37 | true: "Spaces", 38 | false: "Tabs", 39 | }; 40 | 41 | type Props = { 42 | settings: SettingsState; 43 | accessToken: (token: string) => void; 44 | loadSnippets: () => void; 45 | gistId: (id: string) => void; 46 | autosaveTimer: (timer: number) => void; 47 | fontSize: (fontSize: number) => void; 48 | softTabs: (softTabs: boolean) => void; 49 | theme: (theme: string) => void; 50 | autoComplete: (autoComplete: boolean) => void; 51 | lineWrap: (lineWrap: boolean) => void; 52 | linter: (linter: boolean) => void; 53 | loadLegacySnippets: () => void; 54 | }; 55 | 56 | type State = { 57 | showAccessToken: boolean; 58 | initialAccessToken: string | false; 59 | }; 60 | 61 | class Settings extends React.Component { 62 | constructor(props: Props) { 63 | // @ts-expect-error TS is mad that props is not compatible with RouteComponentProps 64 | super(props); 65 | 66 | this.state = { 67 | showAccessToken: false, 68 | initialAccessToken: props.settings.accessToken, 69 | }; 70 | } 71 | 72 | handleBackButton = () => { 73 | if (this.state.initialAccessToken !== this.props.settings.accessToken) { 74 | this.props.loadSnippets(); 75 | } 76 | this.props.history.push(pages.MAIN); 77 | }; 78 | 79 | handleToggleAccessToken = () => { 80 | this.setState({ showAccessToken: !this.state.showAccessToken }); 81 | }; 82 | 83 | render() { 84 | return ( 85 |
86 | 87 | 88 | 89 | 90 | 91 |

Snippets Settings

92 |
93 |
94 |
95 | 96 | 97 |

{process.env.SNIPPETS_VERSION}

98 |

By Sidney Nemzer

99 | 104 | 105 | 106 |
107 | 108 | 109 | 110 | 111 | 112 | 116 | 117 | 118 | 126 | this.props.accessToken(event.target.value) 127 | } 128 | type={this.state.showAccessToken ? "text" : "password"} 129 | /> 130 | 131 | 132 | 133 | 134 | 135 | 136 | this.props.gistId(event.target.value)} 140 | /> 141 | 142 | 143 | 144 | 145 | 149 | 150 | { 152 | this.props.history.push(pages.MAIN); 153 | this.props.loadSnippets(); 154 | }} 155 | > 156 | 157 | 158 | 159 | 160 | 161 | 162 | 166 | 167 | { 172 | const inputInt = parseInt(event.target.value); 173 | if (!Number.isNaN(inputInt)) { 174 | this.props.autosaveTimer(inputInt); 175 | } 176 | }} 177 | /> 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | { 192 | const inputInt = parseInt(event.target.value); 193 | if (!Number.isNaN(inputInt)) { 194 | this.props.autosaveTimer(inputInt); 195 | } 196 | }} 197 | /> 198 | 199 | 200 | 201 | 202 | 203 | 204 | { 209 | const inputInt = parseInt(event.target.value); 210 | if (!Number.isNaN(inputInt)) { 211 | this.props.fontSize(inputInt); 212 | } 213 | }} 214 | /> 215 | 216 | 217 | 218 | 219 | 220 | 221 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 260 | this.props.autoComplete(!this.props.settings.autoComplete) 261 | } 262 | color="primary" 263 | /> 264 | 265 | 266 | 267 | 268 | 269 | 270 | 273 | this.props.lineWrap(!this.props.settings.lineWrap) 274 | } 275 | color="primary" 276 | /> 277 | 278 | 279 | 280 | 281 | 282 | 283 | 286 | this.props.linter(!this.props.settings.linter) 287 | } 288 | color="primary" 289 | /> 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 301 | 302 | { 304 | this.props.loadLegacySnippets(); 305 | this.props.history.push(pages.MAIN); 306 | }} 307 | > 308 | 309 | 310 | 311 | 312 | 313 | 314 |
315 |
316 | ); 317 | } 318 | } 319 | 320 | const mapStateToProps = (state: RootState) => ({ 321 | settings: state.settings, 322 | }); 323 | 324 | const mapDispatchToProps = { 325 | loadSnippets, 326 | loadLegacySnippets, 327 | ...actions, 328 | }; 329 | 330 | export default connect(mapStateToProps, mapDispatchToProps)(Settings); 331 | -------------------------------------------------------------------------------- /src/mode-javascript-eslint/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "@babel/code-frame": { 6 | "version": "7.10.4", 7 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", 8 | "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", 9 | "requires": { 10 | "@babel/highlight": "^7.10.4" 11 | } 12 | }, 13 | "@babel/helper-validator-identifier": { 14 | "version": "7.10.4", 15 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", 16 | "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==" 17 | }, 18 | "@babel/highlight": { 19 | "version": "7.10.4", 20 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", 21 | "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", 22 | "requires": { 23 | "@babel/helper-validator-identifier": "^7.10.4", 24 | "chalk": "^2.0.0", 25 | "js-tokens": "^4.0.0" 26 | }, 27 | "dependencies": { 28 | "chalk": { 29 | "version": "2.4.2", 30 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 31 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 32 | "requires": { 33 | "ansi-styles": "^3.2.1", 34 | "escape-string-regexp": "^1.0.5", 35 | "supports-color": "^5.3.0" 36 | } 37 | } 38 | } 39 | }, 40 | "@types/color-name": { 41 | "version": "1.1.1", 42 | "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", 43 | "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" 44 | }, 45 | "acorn": { 46 | "version": "7.4.0", 47 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", 48 | "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==" 49 | }, 50 | "acorn-jsx": { 51 | "version": "5.2.0", 52 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", 53 | "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==" 54 | }, 55 | "ajv": { 56 | "version": "6.12.4", 57 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", 58 | "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", 59 | "requires": { 60 | "fast-deep-equal": "^3.1.1", 61 | "fast-json-stable-stringify": "^2.0.0", 62 | "json-schema-traverse": "^0.4.1", 63 | "uri-js": "^4.2.2" 64 | } 65 | }, 66 | "ansi-colors": { 67 | "version": "4.1.1", 68 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 69 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" 70 | }, 71 | "ansi-regex": { 72 | "version": "5.0.0", 73 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 74 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" 75 | }, 76 | "ansi-styles": { 77 | "version": "3.2.1", 78 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 79 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 80 | "requires": { 81 | "color-convert": "^1.9.0" 82 | } 83 | }, 84 | "argparse": { 85 | "version": "1.0.10", 86 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 87 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 88 | "requires": { 89 | "sprintf-js": "~1.0.2" 90 | } 91 | }, 92 | "astral-regex": { 93 | "version": "1.0.0", 94 | "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", 95 | "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==" 96 | }, 97 | "balanced-match": { 98 | "version": "1.0.0", 99 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 100 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 101 | }, 102 | "brace-expansion": { 103 | "version": "1.1.11", 104 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 105 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 106 | "requires": { 107 | "balanced-match": "^1.0.0", 108 | "concat-map": "0.0.1" 109 | } 110 | }, 111 | "callsites": { 112 | "version": "3.1.0", 113 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 114 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" 115 | }, 116 | "chalk": { 117 | "version": "4.1.0", 118 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", 119 | "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", 120 | "requires": { 121 | "ansi-styles": "^4.1.0", 122 | "supports-color": "^7.1.0" 123 | }, 124 | "dependencies": { 125 | "ansi-styles": { 126 | "version": "4.2.1", 127 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", 128 | "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", 129 | "requires": { 130 | "@types/color-name": "^1.1.1", 131 | "color-convert": "^2.0.1" 132 | } 133 | }, 134 | "color-convert": { 135 | "version": "2.0.1", 136 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 137 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 138 | "requires": { 139 | "color-name": "~1.1.4" 140 | } 141 | }, 142 | "color-name": { 143 | "version": "1.1.4", 144 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 145 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 146 | }, 147 | "has-flag": { 148 | "version": "4.0.0", 149 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 150 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 151 | }, 152 | "supports-color": { 153 | "version": "7.1.0", 154 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", 155 | "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", 156 | "requires": { 157 | "has-flag": "^4.0.0" 158 | } 159 | } 160 | } 161 | }, 162 | "color-convert": { 163 | "version": "1.9.3", 164 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 165 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 166 | "requires": { 167 | "color-name": "1.1.3" 168 | } 169 | }, 170 | "color-name": { 171 | "version": "1.1.3", 172 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 173 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 174 | }, 175 | "concat-map": { 176 | "version": "0.0.1", 177 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 178 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 179 | }, 180 | "cross-spawn": { 181 | "version": "7.0.3", 182 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 183 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 184 | "requires": { 185 | "path-key": "^3.1.0", 186 | "shebang-command": "^2.0.0", 187 | "which": "^2.0.1" 188 | } 189 | }, 190 | "debug": { 191 | "version": "4.1.1", 192 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 193 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 194 | "requires": { 195 | "ms": "^2.1.1" 196 | } 197 | }, 198 | "deep-is": { 199 | "version": "0.1.3", 200 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", 201 | "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" 202 | }, 203 | "doctrine": { 204 | "version": "3.0.0", 205 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", 206 | "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", 207 | "requires": { 208 | "esutils": "^2.0.2" 209 | } 210 | }, 211 | "emoji-regex": { 212 | "version": "7.0.3", 213 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", 214 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" 215 | }, 216 | "enquirer": { 217 | "version": "2.3.6", 218 | "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", 219 | "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", 220 | "requires": { 221 | "ansi-colors": "^4.1.1" 222 | } 223 | }, 224 | "escape-string-regexp": { 225 | "version": "1.0.5", 226 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 227 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 228 | }, 229 | "eslint": { 230 | "version": "7.7.0", 231 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.7.0.tgz", 232 | "integrity": "sha512-1KUxLzos0ZVsyL81PnRN335nDtQ8/vZUD6uMtWbF+5zDtjKcsklIi78XoE0MVL93QvWTu+E5y44VyyCsOMBrIg==", 233 | "requires": { 234 | "@babel/code-frame": "^7.0.0", 235 | "ajv": "^6.10.0", 236 | "chalk": "^4.0.0", 237 | "cross-spawn": "^7.0.2", 238 | "debug": "^4.0.1", 239 | "doctrine": "^3.0.0", 240 | "enquirer": "^2.3.5", 241 | "eslint-scope": "^5.1.0", 242 | "eslint-utils": "^2.1.0", 243 | "eslint-visitor-keys": "^1.3.0", 244 | "espree": "^7.2.0", 245 | "esquery": "^1.2.0", 246 | "esutils": "^2.0.2", 247 | "file-entry-cache": "^5.0.1", 248 | "functional-red-black-tree": "^1.0.1", 249 | "glob-parent": "^5.0.0", 250 | "globals": "^12.1.0", 251 | "ignore": "^4.0.6", 252 | "import-fresh": "^3.0.0", 253 | "imurmurhash": "^0.1.4", 254 | "is-glob": "^4.0.0", 255 | "js-yaml": "^3.13.1", 256 | "json-stable-stringify-without-jsonify": "^1.0.1", 257 | "levn": "^0.4.1", 258 | "lodash": "^4.17.19", 259 | "minimatch": "^3.0.4", 260 | "natural-compare": "^1.4.0", 261 | "optionator": "^0.9.1", 262 | "progress": "^2.0.0", 263 | "regexpp": "^3.1.0", 264 | "semver": "^7.2.1", 265 | "strip-ansi": "^6.0.0", 266 | "strip-json-comments": "^3.1.0", 267 | "table": "^5.2.3", 268 | "text-table": "^0.2.0", 269 | "v8-compile-cache": "^2.0.3" 270 | } 271 | }, 272 | "eslint-scope": { 273 | "version": "5.1.0", 274 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", 275 | "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", 276 | "requires": { 277 | "esrecurse": "^4.1.0", 278 | "estraverse": "^4.1.1" 279 | } 280 | }, 281 | "eslint-utils": { 282 | "version": "2.1.0", 283 | "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", 284 | "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", 285 | "requires": { 286 | "eslint-visitor-keys": "^1.1.0" 287 | } 288 | }, 289 | "eslint-visitor-keys": { 290 | "version": "1.3.0", 291 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", 292 | "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==" 293 | }, 294 | "espree": { 295 | "version": "7.3.0", 296 | "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", 297 | "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", 298 | "requires": { 299 | "acorn": "^7.4.0", 300 | "acorn-jsx": "^5.2.0", 301 | "eslint-visitor-keys": "^1.3.0" 302 | } 303 | }, 304 | "esprima": { 305 | "version": "4.0.1", 306 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 307 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" 308 | }, 309 | "esquery": { 310 | "version": "1.3.1", 311 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", 312 | "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", 313 | "requires": { 314 | "estraverse": "^5.1.0" 315 | }, 316 | "dependencies": { 317 | "estraverse": { 318 | "version": "5.2.0", 319 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", 320 | "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" 321 | } 322 | } 323 | }, 324 | "esrecurse": { 325 | "version": "4.2.1", 326 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", 327 | "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", 328 | "requires": { 329 | "estraverse": "^4.1.0" 330 | } 331 | }, 332 | "estraverse": { 333 | "version": "4.3.0", 334 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", 335 | "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" 336 | }, 337 | "esutils": { 338 | "version": "2.0.3", 339 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 340 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" 341 | }, 342 | "fast-deep-equal": { 343 | "version": "3.1.3", 344 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 345 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 346 | }, 347 | "fast-json-stable-stringify": { 348 | "version": "2.1.0", 349 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 350 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 351 | }, 352 | "fast-levenshtein": { 353 | "version": "2.0.6", 354 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 355 | "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" 356 | }, 357 | "file-entry-cache": { 358 | "version": "5.0.1", 359 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", 360 | "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", 361 | "requires": { 362 | "flat-cache": "^2.0.1" 363 | } 364 | }, 365 | "flat-cache": { 366 | "version": "2.0.1", 367 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", 368 | "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", 369 | "requires": { 370 | "flatted": "^2.0.0", 371 | "rimraf": "2.6.3", 372 | "write": "1.0.3" 373 | } 374 | }, 375 | "flatted": { 376 | "version": "2.0.2", 377 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", 378 | "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==" 379 | }, 380 | "fs.realpath": { 381 | "version": "1.0.0", 382 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 383 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 384 | }, 385 | "functional-red-black-tree": { 386 | "version": "1.0.1", 387 | "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", 388 | "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" 389 | }, 390 | "glob": { 391 | "version": "7.1.6", 392 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 393 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 394 | "requires": { 395 | "fs.realpath": "^1.0.0", 396 | "inflight": "^1.0.4", 397 | "inherits": "2", 398 | "minimatch": "^3.0.4", 399 | "once": "^1.3.0", 400 | "path-is-absolute": "^1.0.0" 401 | } 402 | }, 403 | "glob-parent": { 404 | "version": "5.1.1", 405 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", 406 | "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", 407 | "requires": { 408 | "is-glob": "^4.0.1" 409 | } 410 | }, 411 | "globals": { 412 | "version": "12.4.0", 413 | "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", 414 | "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", 415 | "requires": { 416 | "type-fest": "^0.8.1" 417 | } 418 | }, 419 | "has-flag": { 420 | "version": "3.0.0", 421 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 422 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 423 | }, 424 | "ignore": { 425 | "version": "4.0.6", 426 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", 427 | "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" 428 | }, 429 | "import-fresh": { 430 | "version": "3.2.1", 431 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", 432 | "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", 433 | "requires": { 434 | "parent-module": "^1.0.0", 435 | "resolve-from": "^4.0.0" 436 | } 437 | }, 438 | "imurmurhash": { 439 | "version": "0.1.4", 440 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 441 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" 442 | }, 443 | "inflight": { 444 | "version": "1.0.6", 445 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 446 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 447 | "requires": { 448 | "once": "^1.3.0", 449 | "wrappy": "1" 450 | } 451 | }, 452 | "inherits": { 453 | "version": "2.0.4", 454 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 455 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 456 | }, 457 | "is-extglob": { 458 | "version": "2.1.1", 459 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 460 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" 461 | }, 462 | "is-fullwidth-code-point": { 463 | "version": "2.0.0", 464 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 465 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" 466 | }, 467 | "is-glob": { 468 | "version": "4.0.1", 469 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", 470 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", 471 | "requires": { 472 | "is-extglob": "^2.1.1" 473 | } 474 | }, 475 | "isexe": { 476 | "version": "2.0.0", 477 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 478 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 479 | }, 480 | "js-tokens": { 481 | "version": "4.0.0", 482 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 483 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 484 | }, 485 | "js-yaml": { 486 | "version": "3.14.0", 487 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", 488 | "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", 489 | "requires": { 490 | "argparse": "^1.0.7", 491 | "esprima": "^4.0.0" 492 | } 493 | }, 494 | "json-schema-traverse": { 495 | "version": "0.4.1", 496 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 497 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 498 | }, 499 | "json-stable-stringify-without-jsonify": { 500 | "version": "1.0.1", 501 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 502 | "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" 503 | }, 504 | "levn": { 505 | "version": "0.4.1", 506 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 507 | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 508 | "requires": { 509 | "prelude-ls": "^1.2.1", 510 | "type-check": "~0.4.0" 511 | } 512 | }, 513 | "lodash": { 514 | "version": "4.17.20", 515 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", 516 | "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" 517 | }, 518 | "minimatch": { 519 | "version": "3.0.4", 520 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 521 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 522 | "requires": { 523 | "brace-expansion": "^1.1.7" 524 | } 525 | }, 526 | "minimist": { 527 | "version": "1.2.5", 528 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 529 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 530 | }, 531 | "mkdirp": { 532 | "version": "0.5.5", 533 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 534 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 535 | "requires": { 536 | "minimist": "^1.2.5" 537 | } 538 | }, 539 | "ms": { 540 | "version": "2.1.2", 541 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 542 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 543 | }, 544 | "natural-compare": { 545 | "version": "1.4.0", 546 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 547 | "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" 548 | }, 549 | "once": { 550 | "version": "1.4.0", 551 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 552 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 553 | "requires": { 554 | "wrappy": "1" 555 | } 556 | }, 557 | "optionator": { 558 | "version": "0.9.1", 559 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", 560 | "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", 561 | "requires": { 562 | "deep-is": "^0.1.3", 563 | "fast-levenshtein": "^2.0.6", 564 | "levn": "^0.4.1", 565 | "prelude-ls": "^1.2.1", 566 | "type-check": "^0.4.0", 567 | "word-wrap": "^1.2.3" 568 | } 569 | }, 570 | "parent-module": { 571 | "version": "1.0.1", 572 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 573 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 574 | "requires": { 575 | "callsites": "^3.0.0" 576 | } 577 | }, 578 | "path-is-absolute": { 579 | "version": "1.0.1", 580 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 581 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 582 | }, 583 | "path-key": { 584 | "version": "3.1.1", 585 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 586 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" 587 | }, 588 | "prelude-ls": { 589 | "version": "1.2.1", 590 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 591 | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" 592 | }, 593 | "progress": { 594 | "version": "2.0.3", 595 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 596 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" 597 | }, 598 | "punycode": { 599 | "version": "2.1.1", 600 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 601 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 602 | }, 603 | "regexpp": { 604 | "version": "3.1.0", 605 | "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", 606 | "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==" 607 | }, 608 | "resolve-from": { 609 | "version": "4.0.0", 610 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 611 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" 612 | }, 613 | "rimraf": { 614 | "version": "2.6.3", 615 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", 616 | "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", 617 | "requires": { 618 | "glob": "^7.1.3" 619 | } 620 | }, 621 | "semver": { 622 | "version": "7.3.2", 623 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", 624 | "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" 625 | }, 626 | "shebang-command": { 627 | "version": "2.0.0", 628 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 629 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 630 | "requires": { 631 | "shebang-regex": "^3.0.0" 632 | } 633 | }, 634 | "shebang-regex": { 635 | "version": "3.0.0", 636 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 637 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" 638 | }, 639 | "slice-ansi": { 640 | "version": "2.1.0", 641 | "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", 642 | "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", 643 | "requires": { 644 | "ansi-styles": "^3.2.0", 645 | "astral-regex": "^1.0.0", 646 | "is-fullwidth-code-point": "^2.0.0" 647 | } 648 | }, 649 | "sprintf-js": { 650 | "version": "1.0.3", 651 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 652 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" 653 | }, 654 | "string-width": { 655 | "version": "3.1.0", 656 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 657 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 658 | "requires": { 659 | "emoji-regex": "^7.0.1", 660 | "is-fullwidth-code-point": "^2.0.0", 661 | "strip-ansi": "^5.1.0" 662 | }, 663 | "dependencies": { 664 | "ansi-regex": { 665 | "version": "4.1.0", 666 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 667 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" 668 | }, 669 | "strip-ansi": { 670 | "version": "5.2.0", 671 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 672 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 673 | "requires": { 674 | "ansi-regex": "^4.1.0" 675 | } 676 | } 677 | } 678 | }, 679 | "strip-ansi": { 680 | "version": "6.0.0", 681 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 682 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 683 | "requires": { 684 | "ansi-regex": "^5.0.0" 685 | } 686 | }, 687 | "strip-json-comments": { 688 | "version": "3.1.1", 689 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 690 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" 691 | }, 692 | "supports-color": { 693 | "version": "5.5.0", 694 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 695 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 696 | "requires": { 697 | "has-flag": "^3.0.0" 698 | } 699 | }, 700 | "table": { 701 | "version": "5.4.6", 702 | "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", 703 | "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", 704 | "requires": { 705 | "ajv": "^6.10.2", 706 | "lodash": "^4.17.14", 707 | "slice-ansi": "^2.1.0", 708 | "string-width": "^3.0.0" 709 | } 710 | }, 711 | "text-table": { 712 | "version": "0.2.0", 713 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 714 | "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" 715 | }, 716 | "type-check": { 717 | "version": "0.4.0", 718 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 719 | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 720 | "requires": { 721 | "prelude-ls": "^1.2.1" 722 | } 723 | }, 724 | "type-fest": { 725 | "version": "0.8.1", 726 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", 727 | "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" 728 | }, 729 | "uri-js": { 730 | "version": "4.2.2", 731 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 732 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 733 | "requires": { 734 | "punycode": "^2.1.0" 735 | } 736 | }, 737 | "v8-compile-cache": { 738 | "version": "2.1.1", 739 | "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", 740 | "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==" 741 | }, 742 | "which": { 743 | "version": "2.0.2", 744 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 745 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 746 | "requires": { 747 | "isexe": "^2.0.0" 748 | } 749 | }, 750 | "word-wrap": { 751 | "version": "1.2.3", 752 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", 753 | "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" 754 | }, 755 | "wrappy": { 756 | "version": "1.0.2", 757 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 758 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 759 | }, 760 | "write": { 761 | "version": "1.0.3", 762 | "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", 763 | "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", 764 | "requires": { 765 | "mkdirp": "^0.5.1" 766 | } 767 | } 768 | } 769 | } 770 | --------------------------------------------------------------------------------