feat: initial commit

This commit is contained in:
Tim Nolet
2018-08-13 21:27:58 +02:00
commit 750a1c26b8
30 changed files with 12399 additions and 0 deletions

7
.babelrc Normal file
View File

@@ -0,0 +1,7 @@
{
"presets": [ "es2015", "stage-0"],
"plugins": [
"transform-runtime",
"transform-object-rest-spread"
]
}

32
.eslintrc.js Normal file
View File

@@ -0,0 +1,32 @@
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
},
env: {
browser: true,
node: true,
"jest/globals": true
},
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
extends: 'standard',
// required to lint *.vue files
plugins: [
'html',
'jest'
],
// add your custom rules here
'rules': {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
},
globals: {
chrome: false
}
}

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules
.DS_Store
.idea
build
*.pem
*.crx

20
README.md Normal file
View File

@@ -0,0 +1,20 @@
# Puppeteer Recorder
Puppeteer recorder is a Chrome extension that records your browser interactions and generates a
[Puppeteer](https://github.com/GoogleChrome/puppeteer) script.
## Known issues
- When navigating between pages, the script is only injected when the full navigation is done, 'committed' in Chrome extension
speak. This means you might be able to see the page and click on stuff, but no events are recorded.
- Restarting a recording reloads the extension in the background. This is annoying and has to do with state, handlers
and open message connections between parts of the extension misfiring.
## Credits & disclaimer
Puppeteer recorder is the spiritual successor & love child of segment.io's
[Daydream](https://github.com/segmentio/daydream) and [ui recorder](https://github.com/yguan/ui-recorder).

10855
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

71
package.json Normal file
View File

@@ -0,0 +1,71 @@
{
"name": "puppeteer-recorder",
"version": "0.1.0",
"description": "A Chrome extension for recording browser interaction and generating Puppeteer scripts",
"main": "index.js",
"scripts": {
"dev": "NODE_ENV=development DEBUG=puppeteer-recorder:* webpack --watch",
"build": "NODE_ENV=production webpack",
"test": "jest"
},
"repository": {
"type": "git",
"url": "https://github.com/checkly/puppeteer-recorder"
},
"keywords": [
"puppeteer",
"chrome",
"extension"
],
"author": "Tim Nolet",
"license": "MIT",
"bugs": {
"url": "https://github.com/checkly/puppeteer-recorder/issues"
},
"homepage": "https://github.com/checkly/puppeteer-recorder#readme",
"dependencies": {
"css-selector-generator": "^1.0.2",
"debug": "^3.1.0",
"vue": "^2.5.17",
"vue-clipboard2": "^0.2.1",
"vue-highlightjs": "^1.3.3"
},
"devDependencies": {
"babel": "^6.23.0",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.6",
"babel-loader": "^7.1.5",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"copy-webpack-plugin": "^4.5.2",
"css-loader": "^1.0.0",
"eslint": "^5.3.0",
"eslint-config-standard": "^11.0.0",
"eslint-plugin-html": "^4.0.5",
"eslint-plugin-import": "^2.13.0",
"eslint-plugin-jest": "^21.21.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-standard": "^3.1.0",
"extract-text-webpack-plugin": "^3.0.2",
"html-webpack-plugin": "^3.2.0",
"jest": "^23.5.0",
"node-sass": "^4.9.3",
"sass-loader": "^7.1.0",
"style-loader": "^0.22.1",
"vue-loader": "^15.3.0",
"vue-style-loader": "^4.1.1",
"vue-template-compiler": "^2.5.17",
"webpack": "^4.16.5",
"webpack-chrome-extension-reloader": "^0.8.3",
"webpack-cli": "^3.1.0"
},
"standard": {
"globals": [
"chrome"
]
}
}

84
src/background/index.js Normal file
View File

@@ -0,0 +1,84 @@
let recording = []
let boundedMessageHandler
let boundedNavigationHandler
let scriptInjected = false
function boot () {
chrome.extension.onConnect.addListener(port => {
port.onMessage.addListener(msg => {
if (msg.action && msg.action === 'start') start()
if (msg.action && msg.action === 'stop') stop()
if (msg.action && msg.action === 'restart') restart()
})
})
chrome.browserAction.onClicked.addListener(() => {
chrome.browserAction.setPopup({ popup: 'index.html' })
})
}
function start () {
console.debug('start recording')
if (!scriptInjected) {
chrome.tabs.executeScript({file: 'content-script.js'})
scriptInjected = true
}
chrome.tabs.query({active: true, currentWindow: true}, tabs => {
chrome.tabs.sendMessage(tabs[0].id, { action: 'get-current-url' }, response => {
if (response) sendCurrentUrl(response.href)
})
})
boundedMessageHandler = handleEvent.bind(this)
boundedNavigationHandler = handleNavigation.bind(this)
chrome.runtime.onMessage.addListener(boundedMessageHandler)
chrome.webNavigation.onCompleted.addListener(boundedNavigationHandler)
chrome.browserAction.setIcon({ path: './images/icon-green.png' })
chrome.browserAction.setBadgeText({ text: 'rec' })
chrome.browserAction.setBadgeBackgroundColor({ color: '#FF0000' })
}
function stop () {
console.debug('stop recording')
chrome.runtime.onMessage.removeListener(boundedMessageHandler)
chrome.webNavigation.onCompleted.removeListener(boundedNavigationHandler)
chrome.browserAction.setIcon({ path: './images/icon-black.png' })
chrome.browserAction.setBadgeText({ text: '1' })
chrome.browserAction.setBadgeBackgroundColor({ color: '#45C8F1' })
chrome.storage.local.set({ recording: recording }, () => {
console.debug('recording stored')
})
}
function restart () {
console.debug('restart')
recording = []
chrome.browserAction.setBadgeText({ text: '' })
chrome.storage.local.remove('recording', () => {
console.debug('stored recording cleared')
})
chrome.runtime.reload()
}
function sendCurrentUrl (href) {
handleEvent({ selector: undefined, value: undefined, action: 'click', href })
}
function handleEvent (event) {
console.debug('receiving event', event)
recording.push(event)
chrome.storage.local.set({ recording: recording }, () => {
console.debug('stored recording updated')
})
}
function handleNavigation ({ url, frameId }) {
console.debug(`current frame ${frameId} with url ${url}`)
if (frameId === 0) {
chrome.tabs.executeScript({file: 'content-script.js'})
}
}
console.debug('booting puppeteer-recorder')
boot()

View File

@@ -0,0 +1,6 @@
export default [
'input',
'textarea',
'a',
'button'
]

View File

@@ -0,0 +1,103 @@
export default [
'click',
// 'focus',
// 'blur',
'dblclick',
'change',
// 'keyup',
'keydown',
// 'keypress',
// 'mousedown',
// 'mousemove',
// 'mouseout',
// 'mouseover',
// 'mouseup'
// 'resize',
// 'scroll',
'select',
'submit',
'load',
'unload'
]
// const events = [
// abort,
// afterprint,
// beforeprint,
// beforeunload,
// blur,
// canplay,
// canplaythrough,
// change,
// click,
// contextmenu,
// copy,
// cuechange,
// cut,
// dblclick,
// DOMContentLoaded,
// drag,
// dragend,
// dragenter,
// dragleave,
// dragover,
// dragstart,
// drop,
// durationchange,
// emptied,
// ended,
// error,
// focus,
// focusin,
// focusout,
// formchange,
// forminput,
// hashchange,
// input,
// invalid,
// keydown,
// keypress,
// keyup,
// load,
// loadeddata,
// loadedmetadata,
// loadstart,
// message,
// mousedown,
// mouseenter,
// mouseleave,
// mousemove,
// mouseout,
// mouseover,
// mouseup,
// mousewheel,
// offline,
// online,
// pagehide,
// pageshow,
// paste,
// pause,
// play,
// playing,
// popstate,
// progress,
// ratechange,
// readystatechange,
// redo,
// reset,
// resize,
// scroll,
// seeked,
// seeking,
// select,
// show,
// stalled,
// storage,
// submit,
// suspend,
// timeupdate,
// undo,
// unload,
// volumechange,
// waiting
// ];

View File

@@ -0,0 +1,58 @@
import eventsToRecord from './events-to-record'
import elementsToBindTo from './elements-to-bind-to'
import Selector from 'css-selector-generator'
const selector = new Selector()
class EventRecorder {
start () {
if (!window.eventRecorder) {
console.debug('starting in EventRecorder')
const elements = document.querySelectorAll(elementsToBindTo.join(','))
for (let i = 0; i < elements.length; i++) {
for (let j = 0; j < eventsToRecord.length; j++) {
elements[i].addEventListener(eventsToRecord[j], recordEvent)
}
}
chrome.runtime.onMessage.addListener((msg, sender, resp) => {
console.debug('got message from background')
if (msg.action && msg.action === 'get-current-url') {
resp({ href: window.location.href })
}
})
window.hasEventRecorder = true
}
}
}
function recordEvent (e) {
const msg = {
selector: selector.getSelector(e.target),
value: e.target.value,
action: e.type,
keyCode: e.keyCode ? e.keyCode : null,
href: e.target.href ? e.target.href : null,
coordinates: getCoordinates(e)
}
sendMessage(msg)
}
function getCoordinates (evt) {
const eventsWithCoordinates = {
mouseup: true,
mousedown: true,
mousemove: true,
mouseover: true
}
return eventsWithCoordinates[evt.type] ? { x: evt.clientX, y: evt.clientY } : null
}
function sendMessage (msg) {
console.debug('sending message', msg)
chrome.runtime.sendMessage(msg)
}
const eventRecorder = new EventRecorder()
eventRecorder.start()

252
src/images/Desert.svg Normal file
View File

@@ -0,0 +1,252 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="110px"
height="110px" viewBox="0 0 110 110" style="enable-background:new 0 0 110 110;" xml:space="preserve">
<g id="Artboard" style="display:none;">
<rect x="-963" y="-178" style="display:inline;fill:#808080;" width="1376" height="359"/>
</g>
<g id="R-Multicolor" style="display:none;">
<circle style="display:inline;fill:#32BEA6;" cx="55" cy="55" r="55"/>
<g style="display:inline;">
<path style="fill:#EDBC7C;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
v4h64v-4L83.879,79.879z"/>
<path style="fill:#F8E1C2;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.796,0,1.559,0.316,2.121,0.879
L87,83H23z"/>
<circle style="fill:#FACB1B;" cx="31" cy="31" r="8"/>
<path style="fill:#107665;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.248-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
C82,37.66,79.28,35,76,35z"/>
<path style="fill:#0D9681;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.248-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
</g>
</g>
<g id="Multicolor">
<g>
<path style="fill:#EDBC7C;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
v4h64v-4L83.879,79.879z"/>
<path style="fill:#F8E1C2;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.796,0,1.559,0.316,2.121,0.879
L87,83H23z"/>
<circle style="fill:#FACB1B;" cx="31" cy="31" r="8"/>
<path style="fill:#107665;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.248-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
C82,37.66,79.28,35,76,35z"/>
<path style="fill:#0D9681;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.248-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
</g>
</g>
<g id="Blue" style="display:none;">
<g style="display:inline;">
<g>
<path style="fill:#53BAD4;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
v4h64v-4L83.879,79.879z"/>
</g>
<g>
<path style="fill:#BBE7F2;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
L87,83H23z"/>
</g>
<g>
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
</g>
<g>
<path style="fill:#0081A1;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
C82,37.66,79.28,35,76,35z"/>
</g>
<g>
<path style="fill:#009FC7;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
</g>
</g>
</g>
<g id="R-Blue" style="display:none;">
<g style="display:inline;">
<circle style="fill:#81D2EB;" cx="55" cy="55" r="55"/>
</g>
<g style="display:inline;">
<g>
<path style="fill:#53BAD4;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
v4h64v-4L83.879,79.879z"/>
</g>
<g>
<path style="fill:#BBE7F2;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
L87,83H23z"/>
</g>
<g>
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
</g>
<g>
<path style="fill:#0081A1;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
C82,37.66,79.28,35,76,35z"/>
</g>
<g>
<path style="fill:#009FC7;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
</g>
</g>
</g>
<g id="Green" style="display:none;">
<g style="display:inline;">
<g>
<path style="fill:#5DCFC3;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
v4h64v-4L83.879,79.879z"/>
</g>
<g>
<path style="fill:#AAF0E9;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
L87,83H23z"/>
</g>
<g>
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
</g>
<g>
<path style="fill:#009687;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
C82,37.66,79.28,35,76,35z"/>
</g>
<g>
<path style="fill:#00B8A5;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
</g>
</g>
</g>
<g id="R-Green" style="display:none;">
<g style="display:inline;">
<circle style="fill:#87E0C8;" cx="55" cy="55" r="55"/>
</g>
<g style="display:inline;">
<g>
<path style="fill:#5DCFC3;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
v4h64v-4L83.879,79.879z"/>
</g>
<g>
<path style="fill:#AAF0E9;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
L87,83H23z"/>
</g>
<g>
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
</g>
<g>
<path style="fill:#009687;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
C82,37.66,79.28,35,76,35z"/>
</g>
<g>
<path style="fill:#00B8A5;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
</g>
</g>
</g>
<g id="Red" style="display:none;">
<g style="display:inline;">
<g>
<path style="fill:#E8A099;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
v4h64v-4L83.879,79.879z"/>
</g>
<g>
<path style="fill:#FFD7D4;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
L87,83H23z"/>
</g>
<g>
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
</g>
<g>
<path style="fill:#C23023;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
C82,37.66,79.28,35,76,35z"/>
</g>
<g>
<path style="fill:#E54B44;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
</g>
</g>
</g>
<g id="R-Red" style="display:none;">
<g style="display:inline;">
<circle style="fill:#FABBAF;" cx="55" cy="55" r="55"/>
</g>
<g style="display:inline;">
<g>
<path style="fill:#E8A099;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
v4h64v-4L83.879,79.879z"/>
</g>
<g>
<path style="fill:#FFD7D4;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
L87,83H23z"/>
</g>
<g>
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
</g>
<g>
<path style="fill:#C23023;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
C82,37.66,79.28,35,76,35z"/>
</g>
<g>
<path style="fill:#E54B44;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
</g>
</g>
</g>
<g id="Yellow" style="display:none;">
<g style="display:inline;">
<g>
<path style="fill:#F5C43D;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
v4h64v-4L83.879,79.879z"/>
</g>
<g>
<path style="fill:#FFE9A1;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
L87,83H23z"/>
</g>
<g>
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
</g>
<g>
<path style="fill:#E07000;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
C82,37.66,79.28,35,76,35z"/>
</g>
<g>
<path style="fill:#FA9200;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
</g>
</g>
</g>
<g id="R-Yellow" style="display:none;">
<g style="display:inline;">
<circle style="fill:#FFD75E;" cx="55" cy="55" r="55"/>
</g>
<g style="display:inline;">
<g>
<path style="fill:#F5C43D;" d="M83.879,79.879C83.316,79.316,82.553,79,81.757,79H28.243c-0.795,0-1.559,0.316-2.122,0.879L23,83
v4h64v-4L83.879,79.879z"/>
</g>
<g>
<path style="fill:#FFE9A1;" d="M23,83l3.121-3.121C26.684,79.316,27.447,79,28.243,79h53.515c0.795,0,1.559,0.316,2.122,0.879
L87,83H23z"/>
</g>
<g>
<circle style="fill:#FFFFFF;" cx="31" cy="31" r="8"/>
</g>
<g>
<path style="fill:#E07000;" d="M76,35c-3.59,0-6,2.81-6,5.94v4.08c0,1.09-0.89,1.98-1.98,1.98h-0.04C66.89,47,66,46.11,66,45.02
V31c0-4.42-3.58-8-8-8s-8,3.58-8,8v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h16V71c0-12.17,16-7.83,16-19.85V40.94
C82,37.66,79.28,35,76,35z"/>
</g>
<g>
<path style="fill:#FA9200;" d="M50,31v22.02c0,1.09-0.89,1.98-1.98,1.98h-0.04C46.89,55,46,54.11,46,53.02v-4.08
c0-3.247-2.54-5.94-6-5.94c-3.427,0-6,2.658-6,5.94v10.21C34,71.17,50,66.83,50,79v4h8V23C53.58,23,50,26.58,50,31z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/images/icon-black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
src/images/icon-green.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

12
src/images/settings.svg Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>_ionicons_svg_ios-settings</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="_ionicons_svg_ios-settings" fill="#8492A6" fill-rule="nonzero">
<path d="M29.3583333,16 C29.3583333,14.25 30.45,12.7583333 32,12.1583333 C31.5916667,10.45 30.9166667,8.85 30.025,7.4 C29.4916667,7.63333333 28.925,7.75833333 28.35,7.75833333 C27.3,7.75833333 26.25,7.35833333 25.4416667,6.55833333 C24.2,5.31666667 23.925,3.49166667 24.5916667,1.975 C23.15,1.08333333 21.5416667,0.408333333 19.8416667,1.18423789e-15 C19.25,1.54166667 17.75,2.64166667 16,2.64166667 C14.25,2.64166667 12.75,1.54166667 12.1583333,0 C10.45,0.408333333 8.85,1.08333333 7.4,1.975 C8.075,3.48333333 7.79166667,5.31666667 6.55,6.55833333 C5.75,7.35833333 4.69166667,7.75833333 3.64166667,7.75833333 C3.06666667,7.75833333 2.5,7.64166667 1.96666667,7.4 C1.08333333,8.85833333 0.408333333,10.4583333 0,12.1666667 C1.54166667,12.7583333 2.64166667,14.25 2.64166667,16.0083333 C2.64166667,17.7583333 1.55,19.25 0.00833333333,19.85 C0.416666667,21.5583333 1.09166667,23.1583333 1.98333333,24.6083333 C2.51666667,24.375 3.08333333,24.2583333 3.65,24.2583333 C4.7,24.2583333 5.75,24.6583333 6.55833333,25.4583333 C7.79166667,26.6916667 8.075,28.525 7.40833333,30.0333333 C8.85833333,30.925 10.4666667,31.6 12.1666667,32.0083333 C12.7583333,30.4666667 14.25,29.375 16,29.375 C17.75,29.375 19.2416667,30.4666667 19.8333333,32.0083333 C21.5416667,31.6 23.1416667,30.925 24.5916667,30.0333333 C23.925,28.525 24.2083333,26.7 25.4416667,25.4583333 C26.2416667,24.6583333 27.2916667,24.2583333 28.35,24.2583333 C28.9166667,24.2583333 29.4916667,24.375 30.0166667,24.6083333 C30.9083333,23.1583333 31.5833333,21.55 31.9916667,19.85 C30.4583333,19.25 29.3583333,17.7583333 29.3583333,16 Z M16.075,22.6583333 C12.3833333,22.6583333 9.40833333,19.6666667 9.40833333,15.9916667 C9.40833333,12.3166667 12.3833333,9.325 16.075,9.325 C19.7666667,9.325 22.7416667,12.3166667 22.7416667,15.9916667 C22.7416667,19.6666667 19.7666667,22.6583333 16.075,22.6583333 Z" id="Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

27
src/manifest.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "Puppeteer Recorder",
"version": "1.0.0",
"manifest_version": 2,
"description": "A Chrome extension for recording browser interaction and generating Puppeteer scripts",
"permissions": [
"storage",
"webNavigation",
"tabs",
"*://*/"
],
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"browser_action": {
"default_icon": "images/icon-black.png",
"default_title": "Puppeteer Recorder"
},
"background": {
"scripts": [
"background.js"
],
"persistent": false
},
"options_ui": {
"page": "options.html",
"open_in_tab": true
}
}

22
src/options/options.html Normal file
View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<title>Options for Puppeteer recorder</title>
</head>
<body>
<h1>Options</h1>
<hr>
<h3>Code generator options</h3>
<label>
<input type="checkbox" id="asyncWrapper">
wrap code in async function
</label>
<hr>
<div id="status"></div>
<button id="save">Save</button>
<script src="options.js"></script>
</body>
</html>

29
src/options/options.js Normal file
View File

@@ -0,0 +1,29 @@
// Saves options to chrome.storage
const defaults = {
codeOptions: {
asyncWrapper: true
}
}
function saveOptions () {
const asyncWrapper = document.getElementById('asyncWrapper').checked
chrome.storage.local.set({
codeOptions: {
asyncWrapper
}
}, () => {
var status = document.getElementById('status')
status.textContent = 'Options saved.'
setTimeout(() => { status.textContent = '' }, 750)
})
}
function restoreOptions () {
// Use default value color = 'red' and likesColor = true.
chrome.storage.local.get(defaults, (items) => {
console.log(items)
document.getElementById('asyncWrapper').checked = items.codeOptions.asyncWrapper
})
}
document.addEventListener('DOMContentLoaded', restoreOptions)
document.getElementById('save').addEventListener('click', saveOptions)

131
src/popup/_animations.scss Normal file
View File

@@ -0,0 +1,131 @@
.shake {
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
transform: translate3d(0, 0, 0);
}
@keyframes shake {
10%, 90% {
transform: translate3d(-1px, 0, 0);
}
20%, 80% {
transform: translate3d(2px, 0, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 0, 0);
}
}
.pulse {
animation: pulse 2s ease infinite;
}
@keyframes pulse {
0% {
opacity: .7;
}
50% {
opacity: .4;
}
100% {
opacity: .7;
}
}
.flash-once {
animation: flash-once 3.5s ease 1;
}
@keyframes fade-up {
0% {
transform: translate3d(0, 10px, 0);
opacity: 0;
}
100% {
transform: translate3d(0, 0, 0);
opacity: 1;
}
}
.fade-in {
animation: fade-in .3s ease-in-out;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.spin {
animation-name: spin;
animation-duration: 2000ms;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
@keyframes spin {
from {transform:rotate(0deg);}
to {transform:rotate(360deg);}
}
.bounceIn {
animation-name: bounceIn;
transform-origin: center bottom;
animation-duration: 1s;
animation-fill-mode: both;
animation-iteration-count: 1;
}
@keyframes bounceIn {
0%, 20%, 40%, 60%, 80%, 100% {
-webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
}
0% {
opacity: 1;
-webkit-transform: scale3d(.8, .8, .8);
transform: scale3d(.8, .8, .8);
}
20% {
-webkit-transform: scale3d(1.1, 1.1, 1.1);
transform: scale3d(1.1, 1.1, 1.1);
}
40% {
-webkit-transform: scale3d(.9, .9, .9);
transform: scale3d(.9, .9, .9);
}
60% {
opacity: 1;
-webkit-transform: scale3d(1.03, 1.03, 1.03);
transform: scale3d(1.03, 1.03, 1.03);
}
80% {
-webkit-transform: scale3d(.97, .97, .97);
transform: scale3d(.97, .97, .97);
}
100% {
opacity: 1;
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}

32
src/popup/_buttons.scss Normal file
View File

@@ -0,0 +1,32 @@
button {
&.btn {
display: inline-block;
font-weight: 300;
line-height: 1.25;
text-align: center;
white-space: nowrap;
vertical-align: middle;
user-select: none;
border: 1px solid transparent;
cursor: pointer;
letter-spacing: 1px;
transition: all .15s ease;
&.btn-sm {
padding: .4rem .8rem;
font-size: .8rem;
border-radius: .2rem
}
&.btn-primary {
color: #fff;
background-color: $brand-primary;
border-color: $brand-primary;
}
&.btn-danger {
color: #fff;
background-color: $brand-danger;
border-color: $brand-danger;
}
}
}

View File

@@ -0,0 +1,21 @@
// drop down
.drop-down-enter, .drop-down-leave-to {
transform: translateX(0) translateY(-20px);
transition-timing-function: cubic-bezier(.74,.04,.26,1.05);
opacity: 0;
}
.drop-down-enter-active, .drop-down-leave-active {
transition: all .15s
}
// move left
.move-left-enter, .move-left-leave-to {
transform: translateY(0) translateX(-80px);
transition-timing-function: cubic-bezier(.74,.04,.26,1.05);
opacity: 0;
}
.move-left-enter-active, .move-left-leave-active {
transition: all .15s
}

View File

@@ -0,0 +1,9 @@
.text-muted {
color: $gray;
}
.text-center {
text-align: center;
}

34
src/popup/_variables.scss Normal file
View File

@@ -0,0 +1,34 @@
// spacing
$spacer: 12px;
// colors
$white: #fff;
$black: #1f2d3d;
$red: #FF4949;
$orange: #F19B45;
$yellow: #FFC82C;
$green: #13CE66;
$blue: #45C8F1;
$blue-light: #EBFAFF;
$blue-lightest: #F0F8FF;
$teal: #205E71;
$pink: #FF659D;
$gray-dark: #3C4858;
$gray: #8492A6;
$gray-light: #E0E6ED;
$gray-lighter: #EFF2F7;
$gray-lightest: #F9FAFC;
$brand-primary: $blue;
$brand-success: $green;
$brand-info: $teal;
$brand-warning: $orange;
$brand-danger: $red;
$brand-inverse: $gray-dark;
$brand-accent: $pink;
// typography
$text-muted: $gray;

View File

@@ -0,0 +1,63 @@
const importPuppeteer = `const puppeteer = require('puppeteer');\n`
const header = `const browser = await puppeteer.launch()
const page = await browser.newPage()`
const footer = `
await browser.close()`
const wrappedHeader = `$(async () => {
const browser = await puppeteer.launch()
const page = await browser.newPage()\n`
const wrappedFooter = `
await browser.close()
})()`
const defaults = {
asyncWrapper: true
}
export default class CodeGenerator {
constructor (options) {
this._options = Object.assign(defaults, options)
this._header = this._options.asyncWrapper ? wrappedHeader : header
this._footer = this._options.asyncWrapper ? wrappedFooter : footer
}
generate (events) {
return importPuppeteer + this._header + this._parseEvents(events) + this._footer
}
_parseEvents (events) {
console.debug(`generating code for ${events.length} events`)
let result = ''
for (let event of events) {
const { action, url, selector, value, href, keyCode } = event
switch (action) {
case 'keydown':
result += this._handleKeyDown(selector, value, keyCode)
break
case 'click':
result += this._handleClick(selector, href)
break
case 'goto':
result += ` await page.goto('${url}')\n`
break
case 'reload':
result += ` await page.reload()\n`
break
}
}
return result
}
_handleKeyDown (selector, value, keyCode) {
if (keyCode === 9) return ` await page.type('${selector}', '${value}')\n`
return ''
}
_handleClick (selector, href) {
if (href) return ` await page.goto('${href}')\n`
return ` await page.click('${selector}')\n`
}
}

View File

@@ -0,0 +1,9 @@
import CodeGenerator from '../CodeGenerator'
describe('code-generator', () => {
test('it should generate nothing when there are no events', () => {
const codeGenerator = new CodeGenerator()
const events = []
expect(codeGenerator._parseEvents(events)).toBeFalsy()
})
})

View File

@@ -0,0 +1,290 @@
<template>
<div id="puppeteer-recorder" class="recorder">
<div class="header">
Puppeteer recorder
<div class="left">
<div class="recording-badge" v-show="isRecording">
<span class="red-dot"></span>
recording
</div>
<a href="#" @click="openOptions" class="options-button">
<img src="/images/settings.svg" alt="settings" width="18px">
</a>
</div>
</div>
<div class="main">
<div class="tabs">
<div class="tab record-tab" v-show="!showResultsTab">
<div class="content">
<div class="empty" v-show="!isRecording">
<img src="/images/Desert.svg" alt="desert" width="78px">
<h3>No recorded events yet</h3>
<p class="text-muted">Click record to begin</p>
</div>
<div class="events" v-show="isRecording">
<p class="text-muted text-center" v-show="liveEvents.length === 0">Waiting for events...</p>
<ul class="event-list">
<li v-for="(event, index) in liveEvents" class="event-list-item">
<div class="event-label">
{{index + 1}}.
</div>
<div class="event-description">
<div class="event-action">{{event.action}}</div>
<div class="event-props text-muted">{{event.selector || event.href }}</div>
</div>
</li>
</ul>
</div>
</div>
<div class="footer">
<button class="btn btn-sm" @click="toggleRecord" :class="isRecording ? 'btn-danger' : 'btn-primary'">
{{recordButtonText}}
</button>
<a href="#" @click="showResultsTab = true" v-show="code">view code</a>
</div>
</div>
<div class="tab results-tab" v-show="showResultsTab">
<div class="content">
<pre v-show="!code">
<code>
No code yet...
</code>
</pre>
<pre v-highlightjs="code" v-show="code"><code class="javascript"></code></pre>
</div>
<div class="footer">
<button class="btn btn-sm btn-primary" @click="restart" v-show="code">Restart</button>
<a href="#" v-clipboard:copy='code' @click="setCopying" v-show="code">{{copyLinkText}}</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import CodeGenerator from '../code-generator/CodeGenerator'
import './github-gist.css'
export default {
name: 'App',
data () {
return {
code: '',
showResultsTab: false,
liveEvents: [],
recording: [],
isRecording: false,
isCopying: false,
bus: null
}
},
mounted () {
this.loadState(() => {
if (this.isRecording) {
console.debug('opened in recording state, fetching recording events')
chrome.storage.local.get(['recording', 'code'], ({ recording }) => {
console.debug('loaded recording', recording)
this.liveEvents = recording
})
}
})
this.bus = chrome.extension.connect({ name: 'recordControls' })
},
methods: {
toggleRecord () {
if (this.isRecording) {
this.stop()
} else {
this.start()
}
this.isRecording = !this.isRecording
this.storeState()
},
start () {
console.debug('start recorder')
this.code = ''
this.bus.postMessage({ action: 'start' })
},
stop () {
console.debug('stop recorder')
this.bus.postMessage({ action: 'stop' })
chrome.storage.local.get(['recording', 'codeOptions'], ({ recording, codeOptions }) => {
console.debug('loaded recording', recording)
this.recording = recording
const codeGen = new CodeGenerator(codeOptions)
this.code = codeGen.generate(this.recording)
this.showResultsTab = true
this.storeState()
})
},
restart () {
console.log('restart')
this.bus.postMessage({ action: 'restart' })
this.recording = this.liveEvents = []
this.code = ''
this.showResultsTab = false
this.storeState()
},
openOptions () {
if (chrome.runtime.openOptionsPage) {
chrome.runtime.openOptionsPage()
}
},
loadState (cb) {
chrome.storage.local.get(['controls', 'code'], ({ controls, code }) => {
console.debug('loaded controls', controls)
if (controls) {
this.isRecording = controls.isRecording
}
if (code) {
this.code = code
}
cb()
})
},
storeState () {
chrome.storage.local.set({
code: this.code,
controls: {
isRecording: this.isRecording
}
})
},
setCopying () {
this.isCopying = true
setTimeout(() => { this.isCopying = false }, 1500)
}
},
computed: {
recordButtonText () {
return this.isRecording ? 'Stop' : 'Record'
},
copyLinkText () {
return this.isCopying ? 'copied!' : 'copy to clipboard'
}
}
}
</script>
<style lang="scss" scoped>
@import "../variables";
.recorder {
font-size: 14px;
.header {
background: $gray-lightest;
height: 48px;
display: flex;
justify-content: flex-start;
align-items: center;
padding: 0 $spacer;
font-weight: 500;
.left {
margin-left: auto;
display: flex;
justify-content: flex-start;
align-items: center;
.recording-badge {
color: $brand-danger;
.red-dot {
height: 8px;
width: 8px;
background-color: $brand-danger;
border-radius: 50%;
display: inline-block;
margin-right: .4rem;
vertical-align: middle;
position: relative;
}
}
.options-button {
margin-left: $spacer;
img {
vertical-align: middle;
}
}
}
}
.record-tab {
.content {
min-height: 200px;
.empty {
padding: $spacer;
text-align: center;
}
}
}
.events {
max-height: 400px;
overflow-y: auto;
.event-list {
list-style-type: none;
padding: 0;
margin: 0;
.event-list-item {
padding: 12px;
font-size: 12px;
border-top: 1px solid $gray-light;
display: flex;
flex: 1 1 auto;
height: 32px;
.event-label {
vertical-align: top;
margin-right: $spacer;
}
.event-description {
margin-right: auto;
display: inline-block;
.event-action {
font-weight: bold;
}
.event-props {
white-space: pre;
}
}
}
}
}
.results-tab {
.content {
pre {
padding: 0 $spacer;
font-size: 12px;
}
}
}
.code {
font-family: Consolas, Monaco, monospace;
padding: $spacer;
}
.footer {
background: $gray-lightest;
height: 60px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 $spacer;
font-weight: 500;
border-top: 1px solid $gray-light;
}
}
</style>

View File

@@ -0,0 +1,71 @@
/**
* GitHub Gist Theme
* Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro
*/
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}

15
src/popup/index.js Normal file
View File

@@ -0,0 +1,15 @@
import Vue from 'vue'
import VueHighlightJS from 'vue-highlightjs'
import VueClipboard from 'vue-clipboard2'
import App from './components/App.vue'
import './style.scss'
Vue.config.productionTip = false
Vue.use(VueHighlightJS)
Vue.use(VueClipboard)
/* eslint-disable no-new */
new Vue({
el: '#root',
render: h => h(App)
})

23
src/popup/style.scss Normal file
View File

@@ -0,0 +1,23 @@
@import "variables";
@import "buttons";
@import "typography";
@import "transitions";
@import "animations";
//reset
body {
margin: 0;
font-size: 100%;
color: $gray-dark;
width: 350px;
}
a {
text-decoration: none;
color: $brand-primary;
}
h1, h2, h3 {
margin-top: 0;
}

10
src/popup/template.html Normal file
View File

@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
</head>
<body>
<div id='root'></div>
<!-- built files will be auto injected -->
</body>
</html>

107
webpack.config.babel.js Normal file
View File

@@ -0,0 +1,107 @@
import webpack from 'webpack'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import ChromeExtensionReloader from 'webpack-chrome-extension-reloader'
import CopyPlugin from 'copy-webpack-plugin'
import { VueLoaderPlugin } from 'vue-loader'
import path from 'path'
const { NODE_ENV = 'development' } = process.env
const base = {
context: __dirname,
entry: {
background: './src/background/index.js',
'content-script': './src/content-scripts/index.js',
popup: './src/popup/index.js',
options: './src/options/options.js'
},
output: {
path: path.join(__dirname, 'build'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
]
},
{
test: /\.scss$/,
use: [
{
loader: 'style-loader' // creates style nodes from JS strings
},
{
loader: 'css-loader' // translates CSS into CommonJS
},
{
loader: 'sass-loader' // compiles Sass to CSS
}
]
}
]
},
plugins: [
new CopyPlugin([
{ from: './src/manifest.json', to: './manifest.json' },
{ from: './src/options/options.html', to: './options.html' },
{ from: './src/images', to: 'images' }
]),
new HtmlWebpackPlugin({
template: './src/popup/template.html',
chunks: ['popup']
}),
new VueLoaderPlugin(),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(NODE_ENV)
}
})
]
}
const development = {
...base,
mode: 'development',
devtool: '#eval-module-source-map',
module: {
...base.module
},
plugins: [
...base.plugins,
new webpack.HotModuleReplacementPlugin()
// new ChromeExtensionReloader()
]
}
const production = {
...base,
mode: 'production',
devtool: '#source-map',
plugins: [
...base.plugins,
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
})
]
}
if (NODE_ENV === 'development') {
module.exports = development
} else {
module.exports = production
}