Use database for REPL, update URL structure (#2572)

* fix: move `shelljs` to dev-deps

* fix: move `codemirror` to “external” config only

* fix: move `yootils` to “external” config only

* chore: bump “sirv” dep

* chore: regenerate pkg-lock

* chore: add `.env.example` file

* chore: remove previous dependencies

* chore: add users migration

* feat: add `db` util w/ pool

* feat: handle GitHub OAuth directly, thru JWTs

* fix: hydrate `user` data & update keys

* fix: add timestamp columns to “users" table

* feat: save new gists to database

* feat: find & update gists

* fix: update client for new endpoints

* fix: always send { error } shape

* fix: rename “users.token” => "users.github_token”

* chore: include node-fetch as dev-dep

* fix: remove extra DATABASE_URL key

* fix: upload OAuth popup message

* chore: regenerate lock file

* add database entries for new gists, update REPL URLs

* implement saving and forking

* insert history entry when forking

* add logic for relaxed gists

* remove unnecessary on conflict clause
pull/2685/head
Luke Edwards 5 years ago committed by Rich Harris
parent 52633542e6
commit 1d0da9d022

@ -0,0 +1,12 @@
NODE_ENV=
PORT=
BASEURL=
DATABASE_URL=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
MAPBOX_ACCESS_TOKEN=
JWT_EXP=30d
JWT_ALG=HS512
JWT_KEY=

@ -0,0 +1,25 @@
exports.up = DB => {
DB.sql(`
create table if not exists users (
id serial primary key,
uid character varying(255) not null unique,
name character varying(255),
username character varying(255) not null,
avatar text,
github_token character varying(255),
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone
);
create unique index if not exists users_pkey ON users(id int4_ops);
create unique index if not exists users_uid_key ON users(uid text_ops);
`);
};
exports.down = DB => {
DB.sql(`
drop table if exists users cascade;
drop index if exists users_uid_key;
drop index if exists users_pkey;
`);
};

@ -0,0 +1,24 @@
exports.up = DB => {
DB.sql(`
create table if not exists gists (
id serial primary key,
uid uuid NOT NULL DEFAULT gen_random_uuid(),
user_id integer REFERENCES users(id) not null,
name character varying(255) not null,
files json not null,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone
);
create unique index if not exists gists_pkey ON gists(id int4_ops);
create index if not exists gists_user_id_key ON gists(user_id int4_ops);
`);
};
exports.down = DB => {
DB.sql(`
drop table if exists gists cascade;
drop index if exists gists_user_id_key;
drop index if exists gists_pkey;
`);
};

@ -1360,6 +1360,11 @@
}
}
},
"@polka/redirect": {
"version": "1.0.0-next.0",
"resolved": "https://registry.npmjs.org/@polka/redirect/-/redirect-1.0.0-next.0.tgz",
"integrity": "sha512-ym6ooqMr09+cV+y52p5kszJ0jYcX+nJfm8POrQb7QYowvpPPuneZ71EclHrQSB7a50lcytgR/xtL6AUFdvyEkg=="
},
"@polka/send": {
"version": "1.0.0-next.2",
"resolved": "https://registry.npmjs.org/@polka/send/-/send-1.0.0-next.2.tgz",
@ -1412,6 +1417,25 @@
"integrity": "sha512-szA3x/3miL90ZJxUCzx9haNbK5/zmPieGraZEe4WI+3srN0eGLiT22NXeMHmyhNEopn+IrxqMc7wdVwvPl8meg==",
"dev": true
},
"@types/pg": {
"version": "7.4.14",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-7.4.14.tgz",
"integrity": "sha512-2e4XapP9V/X42IGByC5IHzCzHqLLCNJid8iZBbkk6lkaDMvli8Rk62YE9wjGcLD5Qr5Zaw1ShkQyXy91PI8C0Q==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/pg-types": "*"
}
},
"@types/pg-types": {
"version": "1.11.4",
"resolved": "https://registry.npmjs.org/@types/pg-types/-/pg-types-1.11.4.tgz",
"integrity": "sha512-WdIiQmE347LGc1Vq3Ki8sk3iyCuLgnccqVzgxek6gEHp2H0p3MQ3jniIHt+bRODXKju4kNQ+mp53lmP5+/9moQ==",
"dev": true,
"requires": {
"moment": ">=2.14.0"
}
},
"@types/resolve": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
@ -1533,15 +1557,11 @@
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true
},
"bagpipe": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/bagpipe/-/bagpipe-0.3.5.tgz",
"integrity": "sha1-40HRZPyyTN8E6n4Ft2XsEMiupqE="
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"base": {
"version": "0.11.2",
@ -1604,11 +1624,6 @@
"integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==",
"dev": true
},
"base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
},
"binary-extensions": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
@ -1625,6 +1640,7 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -1692,12 +1708,22 @@
"integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=",
"dev": true
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
},
"buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw=="
},
"builtin-modules": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz",
@ -1883,7 +1909,8 @@
"codemirror": {
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.46.0.tgz",
"integrity": "sha512-3QpMge0vg4QEhHW3hBAtCipJEWjTJrqLLXdIaWptJOblf1vHFeXLNtFhPai/uX2lnFCehWNk4yOdaMR853Z02w=="
"integrity": "sha512-3QpMge0vg4QEhHW3hBAtCipJEWjTJrqLLXdIaWptJOblf1vHFeXLNtFhPai/uX2lnFCehWNk4yOdaMR853Z02w==",
"dev": true
},
"collection-visit": {
"version": "1.0.0",
@ -1925,7 +1952,30 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"config": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/config/-/config-3.1.0.tgz",
"integrity": "sha512-t6oDeNQbsIWa+D/KF4959TANzjSHLv1BA/hvL8tHEA3OUSWgBXELKaONSI6nr9oanbKs0DXonjOWLcrtZ3yTAA==",
"dev": true,
"optional": true,
"requires": {
"json5": "^1.0.1"
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"optional": true,
"requires": {
"minimist": "^1.2.0"
}
}
}
},
"convert-source-map": {
"version": "1.6.0",
@ -1936,16 +1986,6 @@
"safe-buffer": "~5.1.1"
}
},
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"copy-descriptor": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
@ -2007,6 +2047,7 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
@ -2092,11 +2133,6 @@
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
"optional": true
},
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"devalue": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-1.1.0.tgz",
@ -2125,6 +2161,14 @@
"integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==",
"dev": true
},
"ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"requires": {
"safe-buffer": "^5.0.1"
}
},
"electron-to-chromium": {
"version": "1.3.129",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.129.tgz",
@ -2272,21 +2316,6 @@
}
}
},
"express-session": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.16.1.tgz",
"integrity": "sha512-pWvUL8Tl5jUy1MLH7DhgUlpoKeVPUTe+y6WQD9YhcN0C5qAhsh4a8feVjiUXo3TFhIy191YGZ4tewW9edbl2xQ==",
"requires": {
"cookie": "0.3.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.2",
"safe-buffer": "5.1.2",
"uid-safe": "~2.1.5"
}
},
"extend-shallow": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
@ -2452,16 +2481,6 @@
"map-cache": "^0.2.2"
}
},
"fs-extra": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz",
"integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==",
"requires": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"fs-minipass": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz",
@ -2474,7 +2493,8 @@
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"fsevents": {
"version": "2.0.6",
@ -2514,6 +2534,7 @@
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@ -2565,7 +2586,8 @@
"graceful-fs": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
"integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA=="
"integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
"dev": true
},
"growl": {
"version": "1.10.5",
@ -2673,21 +2695,22 @@
"integrity": "sha512-z6YOZ8ZEnejkcCWlGZzYXNa6i+ZaTfiTg3WhlV/YvnNya3W/RbX1bMVUMTuCrg/DrtTCQxaFCkXCz4FtLpcebg==",
"dev": true
},
"httpie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/httpie/-/httpie-1.1.1.tgz",
"integrity": "sha512-KYgUXOhxVPo5mYuFPqnKW14fP5goMGkLc9CRz0WD6b1TCED9nl9wzj4jqr+8LY+AhPJQ/LdCQLRfF2JrBEja5Q=="
},
"ieee754": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
"dev": true
},
"imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
@ -2696,12 +2719,14 @@
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true
},
"interpret": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz",
"integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw=="
"integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==",
"dev": true
},
"invariant": {
"version": "2.2.4",
@ -3017,20 +3042,55 @@
"minimist": "^1.2.0"
}
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"requires": {
"graceful-fs": "^4.1.6"
}
},
"jsonify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
"dev": true
},
"jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
"requires": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^5.6.0"
},
"dependencies": {
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"requires": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"requires": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"kind-of": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
@ -3104,6 +3164,41 @@
"integrity": "sha1-3bG7s+8HRYwBd7oH3hRCLLAz/5s=",
"dev": true
},
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
},
"lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
},
"lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
},
"lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
},
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
},
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
},
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
},
"log-symbols": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
@ -3237,6 +3332,7 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -3361,6 +3457,12 @@
}
}
},
"moment": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==",
"dev": true
},
"mri": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz",
@ -3370,7 +3472,8 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"nanomatch": {
"version": "1.2.13",
@ -3419,7 +3522,22 @@
"node-fetch": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.5.0.tgz",
"integrity": "sha512-YuZKluhWGJwCcUu4RlZstdAxr8bFfOVHakc1mplwHkk8J+tqM1Y5yraYvIUpeX8aY7+crCwiELJq7Vl0o0LWXw=="
"integrity": "sha512-YuZKluhWGJwCcUu4RlZstdAxr8bFfOVHakc1mplwHkk8J+tqM1Y5yraYvIUpeX8aY7+crCwiELJq7Vl0o0LWXw==",
"dev": true
},
"node-pg-migrate": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/node-pg-migrate/-/node-pg-migrate-3.19.0.tgz",
"integrity": "sha512-IIiiP6oHR9JDOqlIpaRFTnIedPIMXsYiMOkCPCSCGA2e+ZuDn/VS07CbDwVwKA+sBfwR0g5KIBx7QZGAS6sesQ==",
"dev": true,
"requires": {
"@types/pg": "^7.4.0",
"config": ">=1.0.0",
"dotenv": ">=1.0.0",
"lodash": "~4.17.0",
"mkdirp": "~0.5.0",
"yargs": "~13.2.0"
}
},
"node-releases": {
"version": "1.1.17",
@ -3480,16 +3598,6 @@
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true
},
"oauth": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
"integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE="
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-copy": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
@ -3573,15 +3681,11 @@
"integrity": "sha1-3LcCTazVDFK00wPwSALJHAV8dl8=",
"dev": true
},
"on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
@ -3639,6 +3743,11 @@
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
"packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
},
"pako": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz",
@ -3696,51 +3805,12 @@
"json-parse-better-errors": "^1.0.1"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"pascalcase": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
"integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
"dev": true
},
"passport": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz",
"integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=",
"requires": {
"passport-strategy": "1.x.x",
"pause": "0.0.1"
}
},
"passport-github": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz",
"integrity": "sha1-jOHj/NYa11eOsd9ZWDnkrqEjVdQ=",
"requires": {
"passport-oauth2": "1.x.x"
}
},
"passport-oauth2": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz",
"integrity": "sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ==",
"requires": {
"base64url": "3.x.x",
"oauth": "0.9.x",
"passport-strategy": "1.x.x",
"uid2": "0.0.x",
"utils-merge": "1.x.x"
}
},
"passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ="
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
@ -3750,7 +3820,8 @@
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"path-key": {
"version": "2.0.1",
@ -3761,7 +3832,8 @@
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"path-type": {
"version": "3.0.0",
@ -3772,10 +3844,61 @@
"pify": "^3.0.0"
}
},
"pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
"pg": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-7.10.0.tgz",
"integrity": "sha512-aE6FZomsyn3OeGv1oM50v7Xu5zR75c15LXdOCwA9GGrfjXsQjzwYpbcTS6OwEMhYfZQS6m/FVU/ilPLiPzJDCw==",
"requires": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "0.1.3",
"pg-pool": "^2.0.4",
"pg-types": "~2.0.0",
"pgpass": "1.x",
"semver": "4.3.2"
},
"dependencies": {
"semver": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz",
"integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c="
}
}
},
"pg-connection-string": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz",
"integrity": "sha1-2hhHsglA5C7hSSvq9l1J2RskXfc="
},
"pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
},
"pg-pool": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-2.0.6.tgz",
"integrity": "sha512-hod2zYQxM8Gt482q+qONGTYcg/qVcV32VHVPtktbBJs0us3Dj7xibISw0BAAXVMCzt8A/jhfJvpZaxUlqtqs0g=="
},
"pg-types": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.0.1.tgz",
"integrity": "sha512-b7y6QM1VF5nOeX9ukMQ0h8a9z89mojrBHXfJeSug4mhL0YpxNBm83ot2TROyoAmX/ZOX3UbwVO4EbH7i1ZZNiw==",
"requires": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
}
},
"pgpass": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz",
"integrity": "sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=",
"requires": {
"split": "^1.0.0"
}
},
"phin": {
"version": "2.9.3",
@ -3831,6 +3954,29 @@
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
"dev": true
},
"postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="
},
"postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU="
},
"postgres-date": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.4.tgz",
"integrity": "sha512-bESRvKVuTrjoBluEcpv2346+6kgB7UlnqWZsnbnCccTNq/pqfj1j6oBaN5+b/NrDXepYUT/HKadqv3iS9lJuVA=="
},
"postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"requires": {
"xtend": "^4.0.0"
}
},
"prismjs": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.16.0.tgz",
@ -3867,11 +4013,6 @@
"once": "^1.3.1"
}
},
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
},
"read-pkg": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
@ -3911,6 +4052,7 @@
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
"integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
"dev": true,
"requires": {
"resolve": "^1.1.6"
}
@ -4043,6 +4185,7 @@
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz",
"integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
@ -4059,11 +4202,6 @@
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
"dev": true
},
"retry": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz",
"integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q="
},
"rimraf": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
@ -4239,8 +4377,7 @@
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
"dev": true
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
},
"serialize-javascript": {
"version": "1.7.0",
@ -4248,18 +4385,6 @@
"integrity": "sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA==",
"dev": true
},
"session-file-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/session-file-store/-/session-file-store-1.2.0.tgz",
"integrity": "sha512-DkYLYFkkK6u9xyraVHemulhlUuuufLukf7SQxOZSx8SPwkswcaIrls882PaQZ72zRKsyhUVNxOUl9w0lQubUFw==",
"requires": {
"bagpipe": "^0.3.5",
"fs-extra": "^4.0.0",
"object-assign": "^4.1.1",
"retry": "^0.10.0",
"write-file-atomic": "1.3.1"
}
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@ -4320,6 +4445,7 @@
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz",
"integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==",
"dev": true,
"requires": {
"glob": "^7.0.0",
"interpret": "^1.0.0",
@ -4348,11 +4474,6 @@
}
}
},
"slide": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz",
"integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc="
},
"snapdragon": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@ -4541,6 +4662,14 @@
"integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==",
"dev": true
},
"split": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
"integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==",
"requires": {
"through": "2"
}
},
"split-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@ -4700,6 +4829,11 @@
}
}
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
},
"timm": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/timm/-/timm-1.6.1.tgz",
@ -4804,19 +4938,6 @@
}
}
},
"uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"requires": {
"random-bytes": "~1.0.0"
}
},
"uid2": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz",
"integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I="
},
"unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@ -4880,11 +5001,6 @@
}
}
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
},
"unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
@ -4958,11 +5074,6 @@
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@ -5047,17 +5158,8 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"write-file-atomic": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.1.tgz",
"integrity": "sha1-fUW6MjFjKN0ex9kPYOvA2EW7dZo=",
"requires": {
"graceful-fs": "^4.1.11",
"imurmurhash": "^0.1.4",
"slide": "^1.1.5"
}
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"xhr": {
"version": "2.5.0",
@ -5096,8 +5198,7 @@
"xtend": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
"dev": true
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
},
"y18n": {
"version": "4.0.0",
@ -5222,11 +5323,6 @@
}
}
}
},
"yootils": {
"version": "0.0.15",
"resolved": "https://registry.npmjs.org/yootils/-/yootils-0.0.15.tgz",
"integrity": "sha512-GvGLuJ7XHJPGEUQ52vh8fh+vPjfikuGcu7yBswfrsNsHqnAoytOVuSb69eM0j8wQIjMz0U3kY3YsfwMhJgfG9w=="
}
}
}

@ -4,7 +4,9 @@
"description": "Docs and examples for Svelte",
"scripts": {
"dev": "sapper dev",
"migrate": "node-pg-migrate -r dotenv/config",
"sapper": "sapper build --legacy",
"update_shimport": "cp node_modules/shimport/index.js __sapper__/build/client/shimport@0.0.14.js",
"update": "node scripts/update_template.js && node scripts/get-contributors.js",
"start": "node __sapper__/build",
"cy:run": "cypress run",
@ -13,22 +15,18 @@
"testsrc": "mocha -r esm test/**"
},
"dependencies": {
"@polka/redirect": "^1.0.0-next.0",
"@polka/send": "^1.0.0-next.2",
"codemirror": "^5.46.0",
"devalue": "^1.1.0",
"do-not-zip": "^1.0.0",
"express-session": "^1.16.1",
"golden-fleece": "^1.0.9",
"marked": "^0.6.2",
"node-fetch": "^2.5.0",
"passport": "^0.4.0",
"passport-github": "^1.1.0",
"httpie": "^1.1.1",
"jsonwebtoken": "^8.5.1",
"marked": "^0.6.1",
"pg": "^7.10.0",
"polka": "^1.0.0-next.2",
"prismjs": "^1.16.0",
"session-file-store": "^1.2.0",
"shelljs": "^0.8.3",
"sirv": "^0.4.0",
"yootils": "0.0.15"
"prismjs": "^1.15.0",
"sirv": "^0.4.0"
},
"devDependencies": {
"@babel/core": "^7.4.4",
@ -44,8 +42,10 @@
"dotenv": "^8.0.0",
"eslint-plugin-svelte3": "^1.0.0",
"esm": "^3.2.22",
"jimp": "^0.6.4",
"mocha": "^6.1.4",
"jimp": "^0.6.0",
"mocha": "^6.1.3",
"node-fetch": "^2.3.0",
"node-pg-migrate": "^3.18.1",
"npm-run-all": "^4.1.5",
"rollup": "^1.11.2",
"rollup-plugin-babel": "^4.3.2",
@ -56,7 +56,8 @@
"rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^4.0.4",
"sapper": "^0.26.0",
"svelte": "^3.2.0"
"shelljs": "^0.8.3",
"svelte": "^3.0.0"
},
"engines": {
"node": ">=10.0.0"

@ -74,9 +74,13 @@ export default {
commonjs(),
json()
],
external: Object.keys(pkg.dependencies).concat(
require('module').builtinModules || Object.keys(process.binding('natives'))
),
external: [
'yootils',
'codemirror',
...Object.keys(pkg.dependencies || {}).concat(
require('module').builtinModules || Object.keys(process.binding('natives'))
)
],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
@ -95,4 +99,4 @@ export default {
!dev && terser()
]
}
};
};

@ -0,0 +1,150 @@
import polka from 'polka';
import devalue from 'devalue';
import send from '@polka/send';
import { get, post } from 'httpie';
import { parse, stringify } from 'querystring';
import { decode, sign, verify } from './token';
import { find, query } from '../utils/db';
const {
BASEURL,
GITHUB_CLIENT_ID,
GITHUB_CLIENT_SECRET,
} = process.env;
const OAuth = 'https://github.com/login/oauth';
function exit(res, code, msg='') {
const error = msg.charAt(0).toUpperCase() + msg.substring(1);
send(res, code, { error });
}
function onError(err, req, res) {
const error = err.message || err;
const code = err.code || err.status || 500;
res.headersSent || send(res, code, { error });
}
/**
* Middleware to determine User validity
*/
export async function isUser(req, res) {
const abort = exit.bind(null, res, 401);
const auth = req.headers.authorization;
if (!auth) return abort('Missing Authorization header');
const [scheme, token] = auth.split(' ');
if (scheme !== 'Bearer' || !token) return abort('Invalid Authorization format');
let data;
const decoded = decode(token, { complete:true });
if (!decoded || !decoded.header) return abort('Invalid token');
try {
data = await verify(token);
} catch (err) {
return abort(err.message);
}
const { uid, username } = data;
if (!uid || !username) return abort('Invalid token payload');
try {
const row = await find(`select * from users where uid = $1 and username = $2 limit 1`, [uid, username]);
return row || abort('Invalid token');
} catch (err) {
console.error('Auth.isUser', err);
return send(res, 500, 'Unknown error occurred');
}
}
export function toUser(obj={}) {
const { uid, username, name, avatar } = obj;
const token = sign({ uid, username });
return { uid, username, name, avatar, token };
}
export function API() {
const app = polka({ onError });
if (GITHUB_CLIENT_ID) {
app.get('/auth/login', (req, res) => {
try {
const Location = `${OAuth}/authorize?` + stringify({
scope: 'read:user',
client_id: GITHUB_CLIENT_ID,
redirect_uri: `${BASEURL}/auth/callback`,
});
send(res, 302, Location, { Location });
} catch (err) {
console.error('GET /auth/login', err);
send(res, 500);
}
});
app.get('/auth/callback', async (req, res) => {
try {
// Trade "code" for "access_token"
const r1 = await post(`${OAuth}/access_token?` + stringify({
code: req.query.code,
client_id: GITHUB_CLIENT_ID,
client_secret: GITHUB_CLIENT_SECRET,
}));
// Now fetch User details
const { access_token } = parse(r1.data);
const r2 = await get('https://api.github.com/user', {
headers: {
'User-Agent': 'svelte.dev',
Authorization: `token ${access_token}`
}
});
const { id, name, avatar_url, login } = r2.data;
// Upsert `users` table
const [user] = await query(`
insert into users(uid, name, username, avatar, github_token)
values ($1, $2, $3, $4, $5) on conflict (uid) do update
set (name, username, avatar, github_token, updated_at) = ($2, $3, $4, $5, now())
returning *
`, [id, name, login, avatar_url, access_token]);
send(res, 200, `
<script>
window.opener.postMessage({
user: ${devalue(toUser(user))}
}, window.location.origin);
</script>
`, {
'Content-Type': 'text/html; charset=utf-8'
});
} catch (err) {
console.error('GET /auth/callback', err);
send(res, 500, err.data, {
'Content-Type': err.headers['content-type'],
'Content-Length': err.headers['content-length']
});
}
});
} else {
// Print "Misconfigured" error
app.get('/auth/login', (req, res) => {
send(res, 500, `
<body style="font-family: sans-serif; background: rgb(255,215,215); border: 2px solid red; margin: 0; padding: 1em;">
<h1>Missing .env file</h1>
<p>In order to use GitHub authentication, you will need to <a target="_blank" href="https://github.com/settings/developers">register an OAuth application</a> and create a local .env file:</p>
<pre>GITHUB_CLIENT_ID=[YOUR_APP_ID]\nGITHUB_CLIENT_SECRET=[YOUR_APP_SECRET]\nBASEURL=http://localhost:3000</pre>
<p>The <code>BASEURL</code> variable should match the callback URL specified for your app.</p>
<p>See also <a target="_blank" href="https://github.com/sveltejs/svelte/tree/master/site#repl-github-integration">here</a></p>
</body>
`, {
'Content-Type': 'text/html; charset=utf-8'
});
});
}
return app;
}

@ -0,0 +1,22 @@
import * as jwt from 'jsonwebtoken';
const { JWT_KEY, JWT_ALG, JWT_EXP } = process.env;
const CONFIG = {
expiresIn: JWT_EXP,
issuer: 'https://svelte.dev',
audience: 'https://svelte.dev',
algorithm: JWT_ALG,
};
export const decode = jwt.decode;
export function sign(obj, opts, cb) {
opts = Object.assign({}, opts, CONFIG);
return jwt.sign(obj, JWT_KEY, opts, cb);
}
export function verify(str, opts, cb) {
opts = Object.assign({}, opts, CONFIG);
return jwt.verify(str, JWT_KEY, opts, cb);
}

@ -28,7 +28,7 @@
}
if (gist) {
fetch(`gist/${gist}`).then(r => r.json()).then(data => {
fetch(`repl/${gist}.json`).then(r => r.json()).then(data => {
const { id, description, files } = data;
name = description;

@ -1,10 +1,8 @@
import send from '@polka/send';
import { isUser, toUser } from '../../backend/auth';
export function get(req, res) {
if (!req.session || !req.session.passport || !req.session.passport.user) {
return send(res, 200, 'null');
}
const { id, username, displayName, photo } = req.session.passport.user;
send(res, 200, { id, username, displayName, photo });
export async function get(req, res) {
const user = await isUser(req, res);
res.setHeader('Cache-Control', 'private, no-cache, no-store');
return send(res, 200, user ? toUser(user) : null);
}

@ -6,16 +6,16 @@ const cache = new Map();
export function get(req, res) {
const { slug } = req.params;
try {
let example = cache.get(slug);
let example = cache.get(slug);
if (!example || process.env.NODE_ENV !== 'production') {
example = get_example(slug);
cache.set(slug, example);
}
if (!example || process.env.NODE_ENV !== 'production') {
example = get_example(slug);
if (example) cache.set(slug, example);
}
if (example) {
send(res, 200, example);
} catch (err) {
} else {
send(res, 404, {
error: 'not found'
});

@ -35,7 +35,7 @@ export function get_example(slug) {
const dir = lookup.get(slug);
const title = titles.get(slug);
if (!dir || !title) throw { status: 404, message: 'not found' };
if (!dir || !title) return null;
const files = fs.readdirSync(`content/examples/${dir}`)
.filter(name => name[0] !== '.' && name !== 'meta.json')

@ -1,66 +0,0 @@
import fetch from 'node-fetch';
import { body } from './_utils.js';
import send from '@polka/send';
export async function get(req, res) {
const { id } = req.params;
const headers = {};
const user = req.session && req.session.passport && req.session.passport.user;
if (user) {
headers.Authorization = `token ${user.token}`;
}
const r = await fetch(`https://api.github.com/gists/${id}`, {
headers
});
const result = await r.json();
if (r.status === 200) {
send(res, 200, {
id: result.id,
description: result.description,
owner: result.owner,
html_url: result.html_url,
files: result.files
});
} else {
send(res, r.status, result);
}
}
export async function patch(req, res) {
const user = req.session && req.session.passport && req.session.passport.user;
if (!user) {
return send(res, 403, { error: 'unauthorized' });
}
try {
const { description, files } = await body(req);
const r = await fetch(`https://api.github.com/gists/${req.params.id}`, {
method: 'PATCH',
headers: {
Authorization: `token ${user.token}`
},
body: JSON.stringify({
description,
files
})
});
if (r.status === 200) {
send(res, 200, { ok: true });
} else {
send(res, r.status, await r.text(), {
'Content-Type': 'application/json'
});
}
} catch (err) {
send(res, 500, {
error: err.message
});
}
}

@ -1,59 +0,0 @@
import fetch from 'node-fetch';
import { body } from './_utils.js';
import send from '@polka/send';
export async function post(req, res) {
const user = req.session.passport && req.session.passport.user;
if (!user) {
return send(res, 403, { error: 'unauthorized' });
}
try {
const { name, components } = await body(req);
const files = {
'meta.json': {
content: JSON.stringify({
svelte: true
}, null, ' ')
},
'README.md': {
content: `Created with [svelte.dev/repl](https://svelte.dev/repl)`
}
};
components.forEach(component => {
const file = `${component.name}.${component.type}`;
if (!component.source.trim()) {
throw new Error(`GitHub does not allow saving gists with empty files - ${file}`);
}
files[file] = { content: component.source };
});
const r = await fetch(`https://api.github.com/gists`, {
method: 'POST',
headers: {
Authorization: `token ${user.token}`
},
body: JSON.stringify({
description: name,
files,
public: false
})
});
const gist = await r.json();
send(res, r.status, {
id: gist.id,
description: gist.description,
owner: gist.owner,
html_url: gist.html_url,
files: gist.files
});
} catch (err) {
send(res, 500, {
error: err.message
});
}
}

@ -1,15 +1,15 @@
<script>
import { user, logout } from '../../../../user.js';
import { user, logout } from '../../../../../user.js';
let showMenu = false;
let name;
$: name = $user.displayName || $user.username;
$: name = $user.name || $user.username;
</script>
<div class="user" on:mouseenter="{() => showMenu = true}" on:mouseleave="{() => showMenu = false}">
<span>{name}</span>
<img alt="{name} avatar" src="{$user.photo}">
<img alt="{name} avatar" src="{$user.avatar}">
{#if showMenu}
<div class="menu">
@ -99,4 +99,4 @@
display: inline-block;
}
}
</style>
</style>

@ -3,10 +3,10 @@
import UserMenu from './UserMenu.svelte';
import { Icon } from '@sveltejs/site-kit';
import * as doNotZip from 'do-not-zip';
import downloadBlob from '../../_utils/downloadBlob.js';
import { user } from '../../../../user.js';
import { enter } from '../../../../utils/events.js';
import { isMac } from '../../../../utils/compat.js';
import downloadBlob from '../../../_utils/downloadBlob.js';
import { user } from '../../../../../user.js';
import { enter } from '../../../../../utils/events.js';
import { isMac } from '../../../../../utils/compat.js';
const dispatch = createEventDispatcher();
@ -25,8 +25,8 @@
return new Promise(f => setTimeout(f, ms));
}
let canSave;
$: canSave = !!$user && !!gist && !!gist.owner && $user.id == gist.owner.id; // comparing number and string
$: Authorization = $user && `Bearer ${$user.token}`;
$: canSave = $user && gist && gist.owner === $user.uid;
function handleKeydown(event) {
if (event.which === 83 && (isMac ? event.metaKey : event.ctrlKey)) {
@ -54,12 +54,15 @@
const { components } = repl.toJSON();
try {
const r = await fetch(`gist/create`, {
const r = await fetch(`repl/create.json`, {
method: 'POST',
credentials: 'include',
headers: { Authorization },
body: JSON.stringify({
name,
components
files: components.map(component => ({
name: `${component.name}.${component.type}`,
source: component.source
}))
})
});
@ -101,36 +104,21 @@
saving = true;
const { components } = repl.toJSON();
try {
// Send all files back to API
// ~> Any missing files are considered deleted!
const files = {};
const { components } = repl.toJSON();
// null out any deleted files
const set = new Set(components.map(m => `${m.name}.${m.type}`));
Object.keys(gist.files).forEach(file => {
if (/\.(svelte|html|js)$/.test(file)) {
if (!set.has(file)) files[file] = null;
}
});
components.forEach(module => {
const file = `${module.name}.${module.type}`;
if (!module.source.trim()) {
throw new Error(`GitHub does not allow saving gists with empty files - ${file}`);
}
if (!gist.files[file] || module.source !== gist.files[file].content) {
files[file] = { content: module.source };
}
});
const r = await fetch(`gist/${gist.id}`, {
const r = await fetch(`repl/${gist.uid}.json`, {
method: 'PATCH',
credentials: 'include',
headers: { Authorization },
body: JSON.stringify({
description: name,
files
name,
files: components.map(component => ({
name: `${component.name}.${component.type}`,
source: component.source
}))
})
});
@ -212,29 +200,21 @@ export default app;` });
<Icon name="download" />
</button>
{#if $user}
<button class="icon" disabled="{saving || !$user}" on:click={fork} title="fork">
{#if justForked}
<Icon name="check" />
{:else}
<Icon name="git-branch" />
{/if}
</button>
<button class="icon" disabled="{saving || !$user}" on:click={save} title="save">
{#if justSaved}
<Icon name="check" />
{:else}
<Icon name="save" />
{/if}
</button>
{/if}
<button class="icon" disabled="{saving || !$user}" on:click={() => fork(false)} title="fork">
{#if justForked}
<Icon name="check" />
{:else}
<Icon name="git-branch" />
{/if}
</button>
{#if gist}
<a class="icon no-underline" href={gist.html_url} title="link to gist">
<Icon name="link" />
</a>
{/if}
<button class="icon" disabled="{saving || !$user}" on:click={save} title="save">
{#if justSaved}
<Icon name="check" />
{:else}
<Icon name="save" />
{/if}
</button>
{#if $user}
<UserMenu />

@ -0,0 +1,135 @@
import send from '@polka/send';
import redirect from '@polka/redirect';
import body from '../_utils/body.js';
import * as httpie from 'httpie';
import { query, find } from '../../../utils/db';
import { isUser } from '../../../backend/auth';
import { get_example } from '../../examples/_examples.js';
const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } = process.env;
async function import_gist(req, res) {
const base = `https://api.github.com/gists/${req.params.id}`;
const url = `${base}?client_id=${GITHUB_CLIENT_ID}&client_secret=${GITHUB_CLIENT_SECRET}`;
try {
const { data } = await httpie.get(url, {
headers: {
'User-Agent': 'https://svelte.dev'
}
});
// create owner if necessary...
let user = await find(`select * from users where uid = $1`, [data.owner.id]);
if (!user) {
const { id, name, login, avatar_url } = data.owner;
[user] = await query(`
insert into users(uid, name, username, avatar)
values ($1, $2, $3, $4)
returning *
`, [id, name, login, avatar_url]);
}
delete data.files['README.md'];
delete data.files['meta.json'];
const files = Object.keys(data.files).map(key => {
const name = key.replace(/\.html$/, '.svelte');
return {
name,
source: data.files[key].content
};
});
// add gist to database...
const [gist] = await query(`
insert into gists(uid, user_id, name, files)
values ($1, $2, $3, $4) returning *`, [req.params.id, user.id, data.description, JSON.stringify(files)]);
send(res, 200, {
uid: req.params.id,
name: data.description,
files,
owner: data.owner.id
});
} catch (err) {
send(res, err.statusCode, { error: err.message });
}
}
export async function get(req, res) {
// is this an example?
const example = get_example(req.params.id);
if (example) {
return send(res, 200, {
relaxed: true,
uid: req.params.id,
name: example.title,
files: example.files,
owner: null
});
}
const [row] = await query(`
select g.*, u.uid as owner from gists g
left join users u on g.user_id = u.id
where g.uid = $1 limit 1
`, [req.params.id]); // via filename pattern
if (!row) {
return import_gist(req, res);
}
send(res, 200, {
uid: row.uid.replace(/-/g, ''),
name: row.name,
files: row.files,
owner: row.owner
});
}
export async function patch(req, res) {
const user = await isUser(req, res);
if (!user) return; // response already sent
let id, uid=req.params.id;
try {
const [row] = await query(`select * from gists where uid = $1 limit 1`, [uid]);
if (!row) return send(res, 404, { error: 'Gist not found' });
if (row.user_id !== user.id) return send(res, 403, { error: 'Item does not belong to you' });
id = row.id;
} catch (err) {
console.error('PATCH /gists @ select', err);
return send(res, 500);
}
try {
const obj = await body(req);
obj.updated_at = 'now()';
let k, cols=[], vals=[];
for (k in obj) {
cols.push(k);
vals.push(k === 'files' ? JSON.stringify(obj[k]) : obj[k]);
}
const tmp = vals.map((x, i) => `$${i + 1}`).join(',');
const set = `set (${cols.join(',')}) = (${tmp})`;
const [row] = await query(`update gists ${set} where id = ${id} returning *`, vals);
send(res, 200, {
uid: row.uid.replace(/-/g, ''),
name: row.name,
files: row.files,
owner: user.uid,
});
} catch (err) {
console.error('PATCH /gists @ update', err);
send(res, 500, { error: err.message });
}
}

@ -0,0 +1,223 @@
<script context="module">
export function preload({ params, query }) {
return {
version: query.version || '3',
id: params.id
};
}
</script>
<script>
import Repl from '@sveltejs/svelte-repl';
import { onMount } from 'svelte';
import { goto } from '@sapper/app';
import { process_example } from '../../../utils/examples';
import { user } from '../../../user.js';
import InputOutputToggle from '../../../components/Repl/InputOutputToggle.svelte';
import AppControls from './_components/AppControls/index.svelte';
export let version;
export let id;
let repl;
let gist;
let name = 'Loading...';
let zen_mode = false;
let is_relaxed_gist = false;
let width = process.browser ? window.innerWidth : 1000;
let checked = false;
function update_query_string(version) {
const params = [];
if (version !== 'latest') params.push(`version=${version}`);
const url = params.length > 0
? `repl/${id}?${params.join('&')}`
: `repl/${id}`;
history.replaceState({}, 'x', url);
}
$: if (typeof history !== 'undefined') update_query_string(version);
function fetch_gist(id) {
if (gist && gist.uid === id) {
// if the id changed because we just forked, don't refetch
return;
}
// TODO handle `relaxed` logic
fetch(`repl/${id}.json`).then(r => {
if (r.ok) {
r.json().then(data => {
gist = data;
name = data.name;
is_relaxed_gist = data.relaxed;
const rgx = /(js|svelte)$/i;
const components = data.files.map(file => {
let [name, type] = file.name.split('.');
if (type === 'html') type = 'svelte'; // TODO do this on the server
return { name, type, source: file.source };
});
components.sort((a, b) => {
if (a.name === 'App' && a.type === 'svelte') return -1;
if (b.name === 'App' && b.type === 'svelte') return 1;
if (a.type !== b.type) return a.type === 'svelte' ? -1 : 1;
return a.name < b.name ? -1 : 1;
});
repl.set({ components });
});
} else {
console.warn('TODO: 404 Gist')
}
});
}
$: if (process.browser) fetch_gist(id);
onMount(() => {
if (version !== 'local') {
fetch(`https://unpkg.com/svelte@${version || '3'}/package.json`)
.then(r => r.json())
.then(pkg => {
version = pkg.version;
});
}
});
function handle_fork(event) {
console.log('> handle_fork', event);
gist = event.detail.gist;
goto(`/repl/${gist.uid}?version=${version}`);
}
$: svelteUrl = process.browser && version === 'local' ?
`${location.origin}/repl/local` :
`https://unpkg.com/svelte@${version}`;
const rollupUrl = `https://unpkg.com/rollup@1/dist/rollup.browser.js`;
// needed for context API example
const mapbox_setup = `window.MAPBOX_ACCESS_TOKEN = process.env.MAPBOX_ACCESS_TOKEN;`;
$: mobile = width < 540;
$: relaxed = is_relaxed_gist || ($user && gist && $user.uid === gist.owner);
</script>
<style>
.repl-outer {
position: relative;
height: calc(100vh - var(--nav-h));
--app-controls-h: 5.6rem;
--pane-controls-h: 4.2rem;
overflow: hidden;
background-color: var(--back);
padding: var(--app-controls-h) 0 0 0;
/* margin: 0 calc(var(--side-nav) * -1); */
box-sizing: border-box;
}
.viewport {
width: 100%;
height: 100%;
}
.mobile .viewport {
width: 200%;
height: calc(100% - 42px);
transition: transform 0.3s;
}
.mobile .offset {
transform: translate(-50%, 0);
}
/* temp fix for #2499 and #2550 while waiting for a fix for https://github.com/sveltejs/svelte-repl/issues/8 */
.viewport :global(.tab-content),
.viewport :global(.tab-content.visible) {
pointer-events: all;
opacity: 1;
}
.viewport :global(.tab-content) {
visibility: hidden;
}
.viewport :global(.tab-content.visible) {
visibility: visible;
}
.zen-mode {
position: fixed;
width: 100%;
height: 100%;
top: 0;
z-index: 111;
}
.pane { width: 100%; height: 100% }
.loading {
text-align: center;
color: var(--second);
font-weight: 400;
margin: 2em 0 0 0;
opacity: 0;
animation: fade-in .4s;
animation-delay: .2s;
animation-fill-mode: both;
}
@keyframes fade-in {
0% { opacity: 0 }
100% { opacity: 1 }
}
.input {
padding: 2.4em 0 0 0;
}
</style>
<svelte:head>
<title>{name} • REPL • Svelte</title>
<meta name="twitter:title" content="Svelte REPL">
<meta name="twitter:description" content="Cybernetically enhanced web apps">
<meta name="Description" content="Interactive Svelte playground">
</svelte:head>
<svelte:window bind:innerWidth={width}/>
<div class="repl-outer {zen_mode ? 'zen-mode' : ''}" class:mobile>
<AppControls
{gist}
{repl}
bind:name
bind:zen_mode
on:forked={handle_fork}
/>
{#if process.browser}
<div class="viewport" class:offset={checked}>
<Repl
bind:this={repl}
{svelteUrl}
{rollupUrl}
{relaxed}
fixed={mobile}
injectedJS={mapbox_setup}
/>
</div>
{#if mobile}
<InputOutputToggle bind:checked/>
{/if}
{/if}
</div>

@ -1,4 +1,4 @@
export function body(req) {
export default function body(req) {
return new Promise((fulfil, reject) => {
let str = '';

@ -0,0 +1,28 @@
import send from '@polka/send';
import body from './_utils/body.js';
import { query } from '../../utils/db';
import { isUser } from '../../backend/auth';
export async function post(req, res) {
const user = await isUser(req, res);
if (!user) return; // response already sent
try {
const { name, files } = await body(req);
const [row] = await query(`
insert into gists(user_id, name, files)
values ($1, $2, $3) returning *`, [user.id, name, JSON.stringify(files)]);
send(res, 201, {
uid: row.uid.replace(/-/g, ''),
name: row.name,
files: row.files,
owner: user.uid,
});
} catch (err) {
send(res, 500, {
error: err.message
});
}
}

@ -1,240 +1,16 @@
<script context="module">
export function preload({ query }) {
if (/^[^>]?[12]/.test(query.version)) {
const search = Object.keys(query).map(key => `${key}=${query[key]}`).join('&');
return this.redirect(302, `https://v2.svelte.dev/repl?${search}`);
}
return {
version: query.version || '3',
gist_id: query.gist,
example: query.example || 'hello-world'
};
}
</script>
<script>
import Repl from '@sveltejs/svelte-repl';
import { onMount } from 'svelte';
import InputOutputToggle from '../../components/Repl/InputOutputToggle.svelte';
import { process_example } from '../../utils/examples';
import AppControls from './_components/AppControls/index.svelte';
export let version;
export let example;
export let gist_id;
let repl;
let gist;
let name = 'Loading...';
let zen_mode = false;
let relaxed = false;
let width = process.browser ? window.innerWidth : 1000;
let checked = false;
$: if (typeof history !== 'undefined') {
const params = [];
if (version !== 'latest') params.push(`version=${version}`);
if (gist_id) params.push(`gist=${gist_id}`);
else if (example) params.push(`example=${example}`);
const url = params.length > 0
? `repl?${params.join('&')}`
: 'repl';
history.replaceState({}, 'x', url);
}
onMount(() => {
if (version !== 'local') {
fetch(`https://unpkg.com/svelte@${version || '3'}/package.json`)
.then(r => r.json())
.then(pkg => {
version = pkg.version;
});
}
const { gist, example, version } = query;
if (gist_id) {
relaxed = false;
fetch(`gist/${gist_id}`)
.then(r => r.json())
.then(data => {
gist = data;
const { description, files } = data;
name = description;
const components = Object.keys(files)
.map(file => {
const dot = file.lastIndexOf('.');
if (!~dot) return;
const source = files[file].content;
let type = file.slice(dot + 1);
if (type === 'html') type = 'svelte';
return {
name: file.slice(0, dot),
type,
source
};
})
.filter(x => x.type === 'svelte' || x.type === 'js')
.sort((a, b) => {
if (a.name === 'App' && a.type === 'svelte') return -1;
if (b.name === 'App' && b.type === 'svelte') return 1;
if (a.type !== b.type) return a.type === 'svelte' ? -1 : 1;
return a.name < b.name ? -1 : 1;
});
repl.set({ components });
});
} else {
relaxed = true;
fetch(`examples/${example}.json`).then(async response => {
if (response.ok) {
const data = await response.json();
name = data.title;
const components = process_example(data.files);
repl.set({ components });
gist = null;
}
});
// redirect to v2 REPL if appropriate
if (/^[^>]?[12]/.test(version)) {
const q = Object.keys(query).map(key => `${key}=${query[key]}`).join('&');
return this.redirect(302, `https://v2.svelte.dev/repl?${q}`);
}
});
function handle_fork(event) {
example = null;
gist = event.detail.gist;
gist_id = gist.id;
}
$: svelteUrl = process.browser && version === 'local' ?
`${location.origin}/repl/local` :
`https://unpkg.com/svelte@${version}`;
const rollupUrl = `https://unpkg.com/rollup@1/dist/rollup.browser.js`;
const id = gist || example || 'hello-world';
const q = version ? `?version=${version}` : ``;
// needed for context API example
const mapbox_setup = `window.MAPBOX_ACCESS_TOKEN = process.env.MAPBOX_ACCESS_TOKEN;`;
$: mobile = width < 540;
</script>
<style>
.repl-outer {
position: relative;
height: calc(100vh - var(--nav-h));
--app-controls-h: 5.6rem;
--pane-controls-h: 4.2rem;
overflow: hidden;
background-color: var(--back);
padding: var(--app-controls-h) 0 0 0;
/* margin: 0 calc(var(--side-nav) * -1); */
box-sizing: border-box;
}
.viewport {
width: 100%;
height: 100%;
}
.mobile .viewport {
width: 200%;
height: calc(100% - 42px);
transition: transform 0.3s;
}
.mobile .offset {
transform: translate(-50%, 0);
}
/* temp fix for #2499 and #2550 while waiting for a fix for https://github.com/sveltejs/svelte-repl/issues/8 */
.viewport :global(.tab-content),
.viewport :global(.tab-content.visible) {
pointer-events: all;
opacity: 1;
}
.viewport :global(.tab-content) {
visibility: hidden;
}
.viewport :global(.tab-content.visible) {
visibility: visible;
}
.zen-mode {
position: fixed;
width: 100%;
height: 100%;
top: 0;
z-index: 111;
}
.pane { width: 100%; height: 100% }
.loading {
text-align: center;
color: var(--second);
font-weight: 400;
margin: 2em 0 0 0;
opacity: 0;
animation: fade-in .4s;
animation-delay: .2s;
animation-fill-mode: both;
}
@keyframes fade-in {
0% { opacity: 0 }
100% { opacity: 1 }
}
.input {
padding: 2.4em 0 0 0;
this.redirect(301, `repl/${id}${q}`);
}
</style>
<svelte:head>
<title>{name} • REPL • Svelte</title>
<meta name="twitter:title" content="Svelte REPL">
<meta name="twitter:description" content="Cybernetically enhanced web apps">
<meta name="Description" content="Interactive Svelte playground">
</svelte:head>
<svelte:window bind:innerWidth={width}/>
<div class="repl-outer {zen_mode ? 'zen-mode' : ''}" class:mobile>
<AppControls
{gist}
{repl}
bind:name
bind:zen_mode
on:forked={handle_fork}
/>
{#if process.browser}
<div class="viewport" class:offset={checked}>
<Repl
bind:this={repl}
{svelteUrl}
{rollupUrl}
{relaxed}
fixed={mobile}
injectedJS={mapbox_setup}
/>
</div>
{#if mobile}
<InputOutputToggle bind:checked/>
{/if}
{/if}
</div>
</script>

@ -1,119 +1,20 @@
import 'dotenv/config';
import sirv from 'sirv';
import polka from 'polka';
import devalue from 'devalue';
import session from 'express-session';
import passport from 'passport';
import { Strategy } from 'passport-github';
import sessionFileStore from 'session-file-store';
import * as sapper from '@sapper/server';
import { API } from './backend/auth';
const app = polka();
const { PORT=3000 } = process.env;
if (process.env.GITHUB_CLIENT_ID) {
const FileStore = sessionFileStore(session);
API()
.use(
sirv('static', {
setHeaders(res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.hasHeader('Cache-Control') || res.setHeader('Cache-Control', 'max-age=600'); // 10min default
}
}),
passport.use(new Strategy({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: `${process.env.BASEURL}/auth/callback`,
userAgent: 'svelte.dev'
}, (accessToken, refreshToken, profile, callback) => {
return callback(null, {
token: accessToken,
id: profile.id,
username: profile.username,
displayName: profile.displayName,
photo: profile.photos && profile.photos[0] && profile.photos[0].value
});
}));
passport.serializeUser((user, cb) => {
cb(null, user);
});
passport.deserializeUser((obj, cb) => {
cb(null, obj);
});
app
.use(session({
secret: 'svelte',
resave: true,
saveUninitialized: true,
cookie: {
maxAge: 31536000
},
store: new FileStore({
path: process.env.NOW ? `/tmp/sessions` : `.sessions`
})
}))
.use(passport.initialize())
.use(passport.session())
.get('/auth/login', (req, res, next) => {
const { returnTo } = req.query;
req.session.returnTo = returnTo ? decodeURIComponent(returnTo) : '/';
next();
}, passport.authenticate('github', { scope: ['gist', 'read:user'] }))
.post('/auth/logout', (req, res) => {
req.logout();
res.end('ok');
sapper.middleware({
//
})
.get('/auth/callback', passport.authenticate('github', { failureRedirect: '/auth/error' }), (req, res) => {
const { id, username, displayName, photo } = req.session.passport && req.session.passport.user;
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(`
<script>
window.opener.postMessage({
user: ${devalue({ id, username, displayName, photo })}
}, window.location.origin);
</script>
`);
});
} else {
app.get('/auth/login', (req, res) => {
res.writeHead(500);
res.end(`
<body style="font-family: sans-serif; background: rgb(255,215,215); border: 2px solid red; margin: 0; padding: 1em;">
<h1>Missing .env file</h1>
<p>In order to use GitHub authentication, you will need to <a target="_blank" href="https://github.com/settings/developers">register an OAuth application</a> and create a local .env file:</p>
<pre>GITHUB_CLIENT_ID=[YOUR_APP_ID]\nGITHUB_CLIENT_SECRET=[YOUR_APP_SECRET]\nBASEURL=http://localhost:3000</pre>
<p>The <code>BASEURL</code> variable should match the callback URL specified for your app.</p>
<p>See also <a target="_blank" href="https://github.com/sveltejs/svelte/tree/master/site#repl-github-integration">here</a></p>
</body>
`);
});
}
app.use(
sirv('static', {
setHeaders(res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.hasHeader('Cache-Control') || res.setHeader('Cache-Control', 'max-age=600'); // 10min default
}
}),
sapper.middleware({
// TODO update Sapper so that we can pass props to the client
props: req => {
const user = req.session && req.session.passport && req.session.passport.user;
return {
user: user && {
// strip access token
id: user.id,
username: user.username,
displayName: user.displayName,
photo: user.photo
}
};
}
})
).listen(process.env.PORT);
)
.listen(PORT);

@ -1,17 +1,33 @@
import { writable } from 'svelte/store';
export const user = writable(null);
if (process.browser) {
// TODO this is a workaround for the fact that there's currently
// no way to pass session data from server to client
// TODO there is now! replace this with the session mechanism
fetch('/auth/me.json', { credentials: 'include' })
.then(r => r.json())
.then(user.set);
const storageKey = 'svelte-dev:token';
// On load, get the last-known user token (if any)
// Note: We can skip this all by writing User data?
const token = localStorage.getItem(storageKey);
// Write changes to localStorage
user.subscribe(obj => {
if (obj) {
localStorage.setItem(storageKey, obj.token);
} else {
localStorage.removeItem(storageKey);
}
});
if (token) {
// If token, refresh the User data from API
const headers = { Authorization: `Bearer ${token}` };
fetch('/auth/me.json', { headers })
.then(r => r.ok ? r.json() : null)
.then(user.set);
}
}
export async function logout() {
const r = await fetch(`/auth/logout`, { method: 'POST' });
if (r.ok) user.set(null);
}
export function logout() {
user.set(null);
}

@ -0,0 +1,13 @@
import { Pool } from 'pg';
export const DB = new Pool({
connectionString: process.env.DATABASE_URL
});
export function query(text, values=[]) {
return DB.query(text, values).then(r => r.rows);
}
export function find(text, values=[]) {
return query(text, values).then(arr => arr[0]);
}
Loading…
Cancel
Save