├── .gitignore ├── Jenkinsfile ├── Jenkinsfile-k8s ├── README.md ├── index.js ├── kubernetes ├── build-image.sh ├── build-k8s.sh ├── docker │ ├── Dockerfile │ └── scripts │ │ └── service-entrypoint.sh ├── manifests │ ├── balancer.yml.j2 │ └── pod.yml.j2 └── publish.sh ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | 4 | options { 5 | timestamps() 6 | } 7 | 8 | environment { 9 | CI = 'true' 10 | } 11 | 12 | stages { 13 | stage("Test and build") { 14 | parallel { 15 | stage("Build Docker images") { 16 | steps { 17 | sh "kubernetes/build-image.sh" 18 | } 19 | } 20 | } 21 | } 22 | stage("Publish K8S artifacts") { 23 | steps { 24 | withCredentials([file(credentialsId: 'google-container-registry-push', variable: 'GCLOUD_SECRET_FILE')]) { 25 | sh "kubernetes/publish.sh" 26 | } 27 | } 28 | } 29 | } 30 | 31 | post { 32 | failure { 33 | slackSend color: "danger", message: "Build failed - ${env.JOB_NAME} ${env.BUILD_NUMBER} (<${env.RUN_DISPLAY_URL}|Open>)" 34 | } 35 | success { 36 | slackSend color: "good", message: "Build succeeded - ${env.JOB_NAME} ${env.BUILD_NUMBER} (<${env.RUN_DISPLAY_URL}|Open>)" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Jenkinsfile-k8s: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | 4 | options { 5 | timestamps() 6 | } 7 | 8 | environment { 9 | CI = 'true' 10 | } 11 | 12 | parameters { 13 | string(name: 'GIT_REF', defaultValue: 'master', description: 'Repository git reference. Used for pulling the Docker image generated during the git_ref build') 14 | string(name: 'K8S_PROVIDER', defaultValue: 'gke', description: 'K8S provider') 15 | string(name: 'K8S_CLUSTER', defaultValue: 'netlify-origin-gc-dsm-1', description: 'K8S cluster') 16 | string(name: 'K8S_NODEPOOL', defaultValue: 'default', description: 'K8S nodepool') 17 | string(name: 'K8S_ENV', defaultValue: 'staging', description: 'K8S environment') 18 | string(name: 'K8S_DEBUG', defaultValue: 'false', description: 'Enable debug mode on the K8S pods') 19 | string(name: 'GCS_BUCKET', defaultValue: 'gs://netlify-infrastructure', description: 'GCS bucket where the artifacts are stored') 20 | } 21 | 22 | stages { 23 | stage("Build & Publish K8S manifests") { 24 | steps { 25 | withCredentials([file(credentialsId: 'google-container-registry-push', variable: 'GCLOUD_SECRET_FILE')]) { 26 | sh "kubernetes/build-k8s.sh" 27 | } 28 | } 29 | } 30 | } 31 | 32 | post { 33 | always { 34 | archiveArtifacts artifacts: 'spinnaker-artifacts.yml', fingerprint: true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # screenshot 2 | Take screenshots of websites 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer"); 2 | const http = require("http"); 3 | const { URLSearchParams } = require("url"); 4 | 5 | let browser; 6 | 7 | const server = http.createServer(async (req, res) => { 8 | let page, start, url; 9 | 10 | try { 11 | const { host } = req.headers; 12 | const searchParams = new URLSearchParams(req.url.slice(1)); 13 | url = searchParams.get("url"); 14 | const width = parseInt(searchParams.get("width"), 10) || 1024; 15 | const height = parseInt(searchParams.get("height"), 10) || 600; 16 | const delay = searchParams.get("delay") || 0; 17 | const clipRect = searchParams.get("clipRect"); 18 | let clip; 19 | if (clipRect) { 20 | cr = JSON.parse(clipRect); 21 | clip = { 22 | x: cr.left || 0, 23 | y: cr.top || 0, 24 | width: cr.width || width, 25 | height: cr.height || height 26 | }; 27 | } 28 | 29 | page = await browser.newPage(); 30 | 31 | start = Date.now(); 32 | let reqCount = 0; 33 | 34 | const responsePromise = new Promise((resolve, reject) => { 35 | page.on("response", ({ headers }) => { 36 | const location = headers["location"]; 37 | if (location && location.includes(host)) { 38 | reject(new Error("Possible infinite redirects detected.")); 39 | } 40 | }); 41 | }); 42 | 43 | await page.setViewport({ 44 | width, 45 | height 46 | }); 47 | 48 | await Promise.race([ 49 | responsePromise, 50 | page.goto(url, { 51 | waitUntil: "load" 52 | }) 53 | ]); 54 | await new Promise(resolve => setTimeout(resolve, delay)); 55 | 56 | // Pause all media and stop buffering 57 | await Promise.all( 58 | page.frames().map(frame => { 59 | return frame 60 | .evaluate(() => { 61 | document.querySelectorAll("video, audio").forEach(m => { 62 | if (!m) return; 63 | if (m.pause) m.pause(); 64 | m.preload = "none"; 65 | }); 66 | }) 67 | .catch(err => {}); // swallow errors 68 | }) 69 | ); 70 | 71 | const screenshot = await page.screenshot({ 72 | type: "png", 73 | fullPage: false, 74 | clip 75 | }); 76 | 77 | res.writeHead(200, { 78 | "content-type": "image/png", 79 | "cache-control": "public,max-age=31536000" 80 | }); 81 | res.end(screenshot, "binary"); 82 | 83 | const duration = Date.now() - start; 84 | const clipstr = (clip && JSON.stringify(clip)) || "none"; 85 | console.log( 86 | `url=${url} timing=${duration} size=${ 87 | screenshot.length 88 | } status=200 width=${width} height=${height} delay=${delay} clip="${clipstr}"` 89 | ); 90 | } catch (e) { 91 | const { message = "", stack = "" } = e; 92 | res.writeHead(500, { 93 | "content-type": "text/plain" 94 | }); 95 | res.end(`Error generating screenshot.\n\n${message}\n\n${stack}`); 96 | 97 | const duration = Date.now() - start; 98 | console.log(`url=${url} timing=${duration} status=500 error="${message}"`); 99 | } finally { 100 | if (page) { 101 | page.removeAllListeners(); 102 | try { 103 | const cookies = await page.cookies(); 104 | await page.deleteCookie(...cookies); 105 | // tip from https://github.com/GoogleChrome/puppeteer/issues/1490 106 | await page.goto("about:blank", { timeout: 1000 }).catch(err => {}); 107 | } catch (ex) { 108 | // intentionally empty 109 | } finally { 110 | page.close().catch(err => {}); 111 | page = null; 112 | } 113 | } 114 | } 115 | }); 116 | 117 | console.log("Launching Chrome"); 118 | const config = { 119 | ignoreHTTPSErrors: true, 120 | args: [ 121 | "--no-sandbox", 122 | "--disable-setuid-sandbox", 123 | "--disable-gpu", 124 | '--js-flags="--max_old_space_size=500"' 125 | ], 126 | executablePath: process.env.CHROME_BIN 127 | }; 128 | puppeteer 129 | .launch(config) 130 | .then(b => { 131 | browser = b; 132 | const port = process.env.PORT || 3001; 133 | server.listen(port, () => { 134 | console.log(`listening on port ${port}`); 135 | }); 136 | }) 137 | .catch(err => { 138 | console.error("Error launching chrome: ", err); 139 | process.exit(1); 140 | }); 141 | 142 | const stopServer = async () => { 143 | server.close(() => { 144 | process.exit(); 145 | }); 146 | }; 147 | process.on("SIGINT", stopServer); 148 | process.on("SIGTERM", stopServer); 149 | 150 | process.on("unhandledRejection", (reason, p) => { 151 | console.log("Unhandled Rejection at:", p, "reason:", reason); 152 | }); 153 | -------------------------------------------------------------------------------- /kubernetes/build-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | : ${PROJECT_ID:="netlify-services"} 4 | : ${CI:="false"} 5 | : ${GIT_COMMIT:=$(git rev-parse HEAD)} 6 | 7 | set -xe 8 | set -o pipefail 9 | 10 | # If we are building a PR, use the CHANGE_BRANCH value 11 | if [[ -n "$CHANGE_BRANCH" ]]; then 12 | BRANCH_NAME=$CHANGE_BRANCH 13 | fi 14 | 15 | SCREENSHOT_NAME_SHA="gcr.io/$PROJECT_ID/screenshot:$GIT_COMMIT" 16 | SCREENSHOT_NAME_BRANCH="gcr.io/$PROJECT_ID/screenshot:${BRANCH_NAME//\//-}" 17 | 18 | # Build screenshot image 19 | docker build -f kubernetes/docker/Dockerfile \ 20 | -t $SCREENSHOT_NAME_SHA \ 21 | . 22 | -------------------------------------------------------------------------------- /kubernetes/build-k8s.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | : ${K8S_DEBUG:="false"} 4 | : ${GIT_COMMIT:=$(git rev-parse HEAD)} 5 | : ${K8S_NODEPOOL:="default"} 6 | 7 | export K8S_DEBUG 8 | export K8S_NODEPOOL 9 | export K8S_ENV 10 | export GIT_COMMIT 11 | 12 | function jinja2_cmd() { 13 | python -c "import os; 14 | import sys; 15 | import jinja2; 16 | loader=jinja2.FileSystemLoader('$PWD'); 17 | env = jinja2.Environment(loader=loader, undefined=jinja2.StrictUndefined); 18 | sys.stdout.write( 19 | env.from_string(sys.stdin.read()).render(**os.environ) 20 | )" 21 | } 22 | 23 | set -xe 24 | set -o pipefail 25 | 26 | if [[ $GIT_REF == "" ]]; then 27 | echo "\$GIT_REF not provided" 28 | exit 1 29 | fi 30 | 31 | if [[ $GCS_BUCKET == "" ]]; then 32 | echo "\$GCS_BUCKET not provided" 33 | exit 1 34 | fi 35 | 36 | if [[ $K8S_ENV == "" ]]; then 37 | echo "\$K8S_ENV not provided" 38 | exit 1 39 | fi 40 | 41 | if [[ $K8S_CLUSTER == "" ]]; then 42 | echo "\$K8S_CLUSTER not provided" 43 | exit 1 44 | fi 45 | 46 | if [[ $K8S_PROVIDER != "gke" && $K8S_PROVIDER != "eks" ]]; then 47 | echo "Invalid \$K8S_PROVIDER value ($K8S_PROVIDER). Supported values are: gke, eks" 48 | exit 1 49 | fi 50 | 51 | # Prepare paths 52 | SPINNAKER_GCS_PATH="$GCS_BUCKET/spinnaker-k8s-artifacts" 53 | BB_GCS_PATH="$GCS_BUCKET/screenshot-k8s-artifacts/$GIT_REF" 54 | 55 | # Prepare GCS auth 56 | export CLOUDSDK_CORE_DISABLE_PROMPTS=1 57 | source ${HOME}/google-cloud-sdk/path.bash.inc 58 | gcloud auth activate-service-account --key-file "${GCLOUD_SECRET_FILE}" 59 | 60 | # Get docker image digest 61 | IMAGE_DIGEST=$(gsutil cat $BB_GCS_PATH/image-digest) 62 | 63 | # Prepare K8S manifest 64 | gsutil cp -r $BB_GCS_PATH/templates/* . 65 | 66 | # i.e.: gs://netlify-infrastructure/spinnaker-k8s-artifacts/gke/origin-poc/production/screenshot/master 67 | K8S_GS_PATH_BASE=$SPINNAKER_GCS_PATH/$K8S_PROVIDER/$K8S_CLUSTER/$K8S_ENV/screenshot/$GIT_REF 68 | 69 | # Get git info from the build being deployed 70 | export BUILD_GIT_COMMIT=$(gsutil cat $BB_GCS_PATH/git-commit) 71 | export BUILD_GIT_BRANCH=$(gsutil cat $BB_GCS_PATH/git-branch) 72 | 73 | # Build manifests 74 | suffix=$GIT_COMMIT.$(date +%s%N) # Different jobs may build the same commit SHA. Make the filenames different using a timestamp 75 | jinja2_cmd deployment.$suffix.yml 76 | jinja2_cmd balancer.$suffix.yml 77 | 78 | # Upload manifests 79 | gsutil cp deployment.$suffix.yml $K8S_GS_PATH_BASE/ 80 | gsutil cp balancer.$suffix.yml $K8S_GS_PATH_BASE/ 81 | 82 | echo "Generate spinnaker artifacts" 83 | cat > spinnaker-artifacts.yml < /etc/apt/sources.list.d/chrome.list && curl -s ${CHROME_APT_KEY_URL} | apt-key add - && apt-get update && apt-get install -y google-chrome-stable=${CHROME_VERSION} && apt-get clean 33 | 34 | # Install yarn 35 | ENV YARN_APT_KEY_URL "https://dl.yarnpkg.com/debian/pubkey.gpg" 36 | RUN echo "deb http://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list && curl -s ${YARN_APT_KEY_URL} | apt-key add - && apt-get update && apt-get install -y yarn && apt-get clean 37 | 38 | # Install latest dumb-init version 39 | ENV DUMB_INIT_SHA256 37f2c1f0372a45554f1b89924fbb134fc24c3756efaedf11e07f599494e0eff9 40 | ENV DUMB_INIT_VERSION 1.2.2 41 | RUN wget -q https://github.com/Yelp/dumb-init/releases/download/v${DUMB_INIT_VERSION}/dumb-init_${DUMB_INIT_VERSION}_amd64 -O /bin/dumb-init && chmod +x /bin/dumb-init && echo "${DUMB_INIT_SHA256} /bin/dumb-init" > /tmp/dumb-init.sha256sums && sha256sum -c --quiet /tmp/dumb-init.sha256sums 42 | 43 | # Install gosu from consul-template image 44 | COPY --from=consul-template /bin/gosu /bin/ 45 | 46 | # Create screenshot user, used for starting the service inside container 47 | RUN groupadd netlify && useradd netlify -m -g netlify 48 | 49 | 50 | # Prepare workdir 51 | RUN mkdir -p ${SCREENSHOT_PATH} 52 | WORKDIR ${SCREENSHOT_PATH} 53 | 54 | # Install startup scripts and config 55 | COPY kubernetes/docker/scripts/* /usr/bin/ 56 | RUN chmod +x /usr/bin/*.sh 57 | 58 | # Copy screenshot code to container (trigger only "yarn install" when these files change) 59 | COPY package.json yarn.lock ${SCREENSHOT_PATH}/ 60 | 61 | # Install screenshot service 62 | RUN yarn install 63 | 64 | # Copy node code to container 65 | COPY . ${SCREENSHOT_PATH}/ -------------------------------------------------------------------------------- /kubernetes/docker/scripts/service-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/dumb-init /bin/bash 2 | 3 | set -e 4 | 5 | cd $PWD 6 | exec gosu netlify "$@" 7 | -------------------------------------------------------------------------------- /kubernetes/manifests/balancer.yml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: screenshot 5 | namespace: {{ K8S_ENV }} 6 | labels: 7 | app: screenshot 8 | git_commit: {{ BUILD_GIT_COMMIT }} 9 | git_branch: {{ BUILD_GIT_BRANCH }} 10 | spec: 11 | ports: 12 | - name: http 13 | port: 3002 14 | protocol: TCP 15 | targetPort: 3002 16 | selector: 17 | app: screenshot -------------------------------------------------------------------------------- /kubernetes/manifests/pod.yml.j2: -------------------------------------------------------------------------------- 1 | {% set app_name = "screenshot" -%} 2 | {% set service_port = "3002" %} 3 | {% set resources = {"requests": {"cpu": "500m", "mem": "500M"}, "limits": {"cpu": "1", "mem": "500M"} } %} 4 | --- 5 | apiVersion: extensions/v1beta1 6 | kind: Deployment 7 | metadata: 8 | name: {{ app_name }} 9 | namespace: {{ K8S_ENV }} 10 | annotations: 11 | git_commit: {{ BUILD_GIT_COMMIT }} 12 | git_branch: {{ BUILD_GIT_BRANCH }} 13 | spec: 14 | revisionHistoryLimit: 3 15 | minReadySeconds: 5 16 | strategy: 17 | rollingUpdate: 18 | maxSurge: 15% 19 | maxUnavailable: 0 20 | type: RollingUpdate 21 | template: 22 | metadata: 23 | annotations: 24 | git_commit: {{ BUILD_GIT_COMMIT }} 25 | git_branch: {{ BUILD_GIT_BRANCH }} 26 | labels: 27 | app: {{ app_name }} 28 | environment: {{ K8S_ENV }} 29 | spec: 30 | nodeSelector: 31 | cloud.google.com/gke-nodepool: {{ K8S_NODEPOOL }} 32 | containers: 33 | - name: {{ app_name }} 34 | command: ["/usr/bin/service-entrypoint.sh"] 35 | args: ["/usr/local/bin/node", "index.js"] 36 | image: gcr.io/netlify-services/{{ app_name }} 37 | imagePullPolicy: Always 38 | env: 39 | - name: K8S_PROVIDER 40 | value: {{ K8S_PROVIDER }} 41 | - name: K8S_CLUSTER 42 | value: {{ K8S_CLUSTER }} 43 | - name: K8S_ENV 44 | value: {{ K8S_ENV }} 45 | - name: PORT 46 | value: "{{ service_port }}" 47 | - name: DATADOG_HOST 48 | valueFrom: 49 | fieldRef: 50 | fieldPath: status.hostIP 51 | ports: 52 | - containerPort: {{ service_port }} 53 | readinessProbe: 54 | tcpSocket: 55 | port: {{ service_port }} 56 | periodSeconds: 2 57 | successThreshold: 1 58 | timeoutSeconds: 2 59 | failureThreshold: 5 60 | livenessProbe: 61 | httpGet: 62 | path: "/url=http%3A%2F%2Fwww.google.com&width=1&height=1" 63 | port: {{ service_port }} 64 | periodSeconds: 2 65 | successThreshold: 1 66 | timeoutSeconds: 3 67 | failureThreshold: 5 68 | # Give K8S enough time to remove this Pod before actually sending the SIGTERM 69 | # More info: https://github.com/kubernetes/kubernetes/issues/43576 70 | lifecycle: 71 | preStop: 72 | exec: 73 | command: 74 | - /bin/sleep 75 | - "10" 76 | resources: 77 | requests: 78 | cpu: {{ resources["requests"]["cpu"] }} 79 | memory: {{ resources["requests"]["mem"] }} 80 | limits: 81 | cpu: {{ resources["limits"]["cpu"] }} 82 | memory: {{ resources["limits"]["mem"] }} 83 | --- 84 | apiVersion: policy/v1beta1 85 | kind: PodDisruptionBudget 86 | metadata: 87 | name: {{ app_name }} 88 | namespace: {{ K8S_ENV }} 89 | spec: 90 | minAvailable: 60% 91 | selector: 92 | matchLabels: 93 | app: {{ app_name }} 94 | -------------------------------------------------------------------------------- /kubernetes/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | : ${PROJECT_ID:="netlify-services"} 4 | : ${CI:="false"} 5 | : ${GIT_COMMIT:=$(git rev-parse HEAD)} 6 | : ${GCS_BUCKET:="gs://netlify-infrastructure"} 7 | 8 | # If we are building a PR, use the CHANGE_BRANCH value 9 | if [[ -n "$CHANGE_BRANCH" ]]; then 10 | BRANCH_NAME=$CHANGE_BRANCH 11 | fi 12 | 13 | set -xe 14 | set -o pipefail 15 | 16 | SCREENSHOT_NAME_SHA="gcr.io/$PROJECT_ID/screenshot:$GIT_COMMIT" 17 | SCREENSHOT_NAME_BRANCH="gcr.io/$PROJECT_ID/screenshot:${BRANCH_NAME//\//-}" 18 | 19 | cleanup_images() { 20 | docker rmi $SCREENSHOT_NAME_SHA || true 21 | if [[ $BRANCH_NAME != "master" ]]; then 22 | docker rmi $SCREENSHOT_NAME_BRANCH || true 23 | fi 24 | } 25 | trap cleanup_images EXIT 26 | 27 | export CLOUDSDK_CORE_DISABLE_PROMPTS=1 28 | source ${HOME}/google-cloud-sdk/path.bash.inc 29 | gcloud components install kubectl 30 | gcloud auth activate-service-account --key-file "${GCLOUD_SECRET_FILE}" 31 | 32 | # Prepare path 33 | GCS_PATH="$GCS_BUCKET/screenshot-k8s-artifacts" 34 | 35 | # Upload Docker image 36 | docker tag $SCREENSHOT_NAME_SHA $SCREENSHOT_NAME_BRANCH 37 | gcloud docker -- push $SCREENSHOT_NAME_SHA 38 | gcloud docker -- push $SCREENSHOT_NAME_BRANCH 39 | 40 | # Upload Docker image digest 41 | DOCKER_IMAGE_DIGEST=$(gcloud container images describe $SCREENSHOT_NAME_SHA --format='value(image_summary.fully_qualified_digest)') 42 | echo $DOCKER_IMAGE_DIGEST > image-digest 43 | gsutil cp image-digest $GCS_PATH/$GIT_COMMIT/image-digest 44 | gsutil cp image-digest $GCS_PATH/$BRANCH_NAME/image-digest 45 | 46 | # Upload K8S templates 47 | gsutil cp -r kubernetes/manifests/* $GCS_PATH/$GIT_COMMIT/templates/ 48 | gsutil cp -r kubernetes/manifests/* $GCS_PATH/$BRANCH_NAME/templates/ 49 | 50 | # Save git information 51 | echo $BRANCH_NAME > git-branch 52 | gsutil cp git-branch $GCS_PATH/$GIT_COMMIT/git-branch 53 | gsutil cp git-branch $GCS_PATH/$BRANCH_NAME/git-branch 54 | rm git-branch 55 | echo $GIT_COMMIT > git-commit 56 | gsutil cp git-commit $GCS_PATH/$GIT_COMMIT/git-commit 57 | gsutil cp git-commit $GCS_PATH/$BRANCH_NAME/git-commit 58 | rm git-commit 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "screenshot", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:netlify/screenshot.git", 6 | "author": "Bryce Kahle ", 7 | "license": "UNLICENSED", 8 | "dependencies": { 9 | "puppeteer": "^1.1.1" 10 | }, 11 | "private": true 12 | } 13 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | agent-base@^4.1.0: 6 | version "4.2.0" 7 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce" 8 | dependencies: 9 | es6-promisify "^5.0.0" 10 | 11 | async-limiter@~1.0.0: 12 | version "1.0.0" 13 | resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" 14 | 15 | balanced-match@^1.0.0: 16 | version "1.0.0" 17 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 18 | 19 | brace-expansion@^1.1.7: 20 | version "1.1.11" 21 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 22 | dependencies: 23 | balanced-match "^1.0.0" 24 | concat-map "0.0.1" 25 | 26 | concat-map@0.0.1: 27 | version "0.0.1" 28 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 29 | 30 | concat-stream@1.6.0: 31 | version "1.6.0" 32 | resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" 33 | dependencies: 34 | inherits "^2.0.3" 35 | readable-stream "^2.2.2" 36 | typedarray "^0.0.6" 37 | 38 | core-util-is@~1.0.0: 39 | version "1.0.2" 40 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 41 | 42 | debug@2.6.9, debug@^2.6.8: 43 | version "2.6.9" 44 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 45 | dependencies: 46 | ms "2.0.0" 47 | 48 | debug@^3.1.0: 49 | version "3.1.0" 50 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 51 | dependencies: 52 | ms "2.0.0" 53 | 54 | es6-promise@^4.0.3: 55 | version "4.2.4" 56 | resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29" 57 | 58 | es6-promisify@^5.0.0: 59 | version "5.0.0" 60 | resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" 61 | dependencies: 62 | es6-promise "^4.0.3" 63 | 64 | extract-zip@^1.6.5: 65 | version "1.6.6" 66 | resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c" 67 | dependencies: 68 | concat-stream "1.6.0" 69 | debug "2.6.9" 70 | mkdirp "0.5.0" 71 | yauzl "2.4.1" 72 | 73 | fd-slicer@~1.0.1: 74 | version "1.0.1" 75 | resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" 76 | dependencies: 77 | pend "~1.2.0" 78 | 79 | fs.realpath@^1.0.0: 80 | version "1.0.0" 81 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 82 | 83 | glob@^7.0.5: 84 | version "7.1.2" 85 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 86 | dependencies: 87 | fs.realpath "^1.0.0" 88 | inflight "^1.0.4" 89 | inherits "2" 90 | minimatch "^3.0.4" 91 | once "^1.3.0" 92 | path-is-absolute "^1.0.0" 93 | 94 | https-proxy-agent@^2.1.0: 95 | version "2.1.1" 96 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.1.1.tgz#a7ce4382a1ba8266ee848578778122d491260fd9" 97 | dependencies: 98 | agent-base "^4.1.0" 99 | debug "^3.1.0" 100 | 101 | inflight@^1.0.4: 102 | version "1.0.6" 103 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 104 | dependencies: 105 | once "^1.3.0" 106 | wrappy "1" 107 | 108 | inherits@2, inherits@^2.0.3, inherits@~2.0.3: 109 | version "2.0.3" 110 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 111 | 112 | isarray@~1.0.0: 113 | version "1.0.0" 114 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 115 | 116 | mime@^1.3.4: 117 | version "1.6.0" 118 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 119 | 120 | minimatch@^3.0.4: 121 | version "3.0.4" 122 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 123 | dependencies: 124 | brace-expansion "^1.1.7" 125 | 126 | minimist@0.0.8: 127 | version "0.0.8" 128 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 129 | 130 | mkdirp@0.5.0: 131 | version "0.5.0" 132 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" 133 | dependencies: 134 | minimist "0.0.8" 135 | 136 | ms@2.0.0: 137 | version "2.0.0" 138 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 139 | 140 | once@^1.3.0: 141 | version "1.4.0" 142 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 143 | dependencies: 144 | wrappy "1" 145 | 146 | path-is-absolute@^1.0.0: 147 | version "1.0.1" 148 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 149 | 150 | pend@~1.2.0: 151 | version "1.2.0" 152 | resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" 153 | 154 | process-nextick-args@~2.0.0: 155 | version "2.0.0" 156 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" 157 | 158 | progress@^2.0.0: 159 | version "2.0.0" 160 | resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" 161 | 162 | proxy-from-env@^1.0.0: 163 | version "1.0.0" 164 | resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" 165 | 166 | puppeteer@^1.1.1: 167 | version "1.1.1" 168 | resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.1.1.tgz#adbf25e49f5ef03443c10ab8e09a954ca0c7bfee" 169 | dependencies: 170 | debug "^2.6.8" 171 | extract-zip "^1.6.5" 172 | https-proxy-agent "^2.1.0" 173 | mime "^1.3.4" 174 | progress "^2.0.0" 175 | proxy-from-env "^1.0.0" 176 | rimraf "^2.6.1" 177 | ws "^3.0.0" 178 | 179 | readable-stream@^2.2.2: 180 | version "2.3.4" 181 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071" 182 | dependencies: 183 | core-util-is "~1.0.0" 184 | inherits "~2.0.3" 185 | isarray "~1.0.0" 186 | process-nextick-args "~2.0.0" 187 | safe-buffer "~5.1.1" 188 | string_decoder "~1.0.3" 189 | util-deprecate "~1.0.1" 190 | 191 | rimraf@^2.6.1: 192 | version "2.6.2" 193 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" 194 | dependencies: 195 | glob "^7.0.5" 196 | 197 | safe-buffer@~5.1.0, safe-buffer@~5.1.1: 198 | version "5.1.1" 199 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 200 | 201 | string_decoder@~1.0.3: 202 | version "1.0.3" 203 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" 204 | dependencies: 205 | safe-buffer "~5.1.0" 206 | 207 | typedarray@^0.0.6: 208 | version "0.0.6" 209 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" 210 | 211 | ultron@~1.1.0: 212 | version "1.1.1" 213 | resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" 214 | 215 | util-deprecate@~1.0.1: 216 | version "1.0.2" 217 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 218 | 219 | wrappy@1: 220 | version "1.0.2" 221 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 222 | 223 | ws@^3.0.0: 224 | version "3.3.3" 225 | resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" 226 | dependencies: 227 | async-limiter "~1.0.0" 228 | safe-buffer "~5.1.0" 229 | ultron "~1.1.0" 230 | 231 | yauzl@2.4.1: 232 | version "2.4.1" 233 | resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" 234 | dependencies: 235 | fd-slicer "~1.0.1" 236 | --------------------------------------------------------------------------------