├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── ACKNOWLEDGEMENTS.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── client-service-worker ├── README.md ├── ___dumber-gist-worker.js ├── __boot-up-worker.html ├── __remove-expired-worker.html ├── favicon.ico └── index.html ├── client ├── .eslintrc.json ├── .htmlhintrc ├── .npmrc ├── .sass-lint.yml ├── README.md ├── _index.html ├── favicon.ico ├── gulpfile.js ├── host-names.js ├── jsconfig.json ├── package.json ├── src-worker │ ├── au1-deps-finder.js │ ├── cache-primitives.js │ ├── concat-with-sourcemaps.js │ ├── deps-resolver.js │ ├── dumber-cache.js │ ├── dumber-session.js │ ├── index.js │ ├── jsdelivr.js │ ├── transpiler.js │ ├── transpilers │ │ ├── au2.js │ │ ├── js.js │ │ ├── less.js │ │ ├── sass.js │ │ ├── svelte.js │ │ └── text.js │ └── turbo-resolver │ │ ├── registries │ │ └── npm-http.js │ │ └── resolver.js ├── src │ ├── _dialog.scss │ ├── _gist-editor.scss │ ├── _variables.scss │ ├── action-dispatcher.js │ ├── app.html │ ├── app.js │ ├── app.scss │ ├── dialogs │ │ ├── confirmation-dialog.html │ │ ├── confirmation-dialog.js │ │ ├── context-menu.html │ │ ├── context-menu.js │ │ ├── create-file-dialog.html │ │ ├── create-file-dialog.js │ │ ├── edit-name-dialog.html │ │ ├── edit-name-dialog.js │ │ ├── fuzzy-filter.js │ │ ├── list-gists-dialog.html │ │ ├── list-gists-dialog.js │ │ ├── new-gist-dialog.html │ │ ├── new-gist-dialog.js │ │ ├── open-file-dialog.html │ │ ├── open-file-dialog.js │ │ ├── waiting-dialog.html │ │ └── waiting-dialog.js │ ├── edit │ │ ├── code-editor.html │ │ ├── code-editor.js │ │ ├── code-editor.scss │ │ ├── dialogs │ │ │ ├── editor-config-dialog.html │ │ │ └── editor-config-dialog.js │ │ ├── edit-session.js │ │ ├── editor-tabs.html │ │ ├── editor-tabs.js │ │ ├── file-tree.js │ │ └── opened-files.js │ ├── embedded-browser │ │ ├── browser-bar.html │ │ ├── browser-bar.js │ │ ├── browser-dev-tools.html │ │ ├── browser-dev-tools.js │ │ ├── browser-frame.html │ │ ├── browser-frame.js │ │ ├── console-log.js │ │ ├── dialogs │ │ │ ├── browser-config-dialog.html │ │ │ └── browser-config-dialog.js │ │ ├── log-line.html │ │ ├── log-line.js │ │ ├── logs.html │ │ └── logs.js │ ├── file-drop-indicator.html │ ├── file-drop-indicator.js │ ├── gist-app.html │ ├── gist-app.js │ ├── github │ │ ├── access-token.js │ │ ├── api-client.js │ │ ├── gists.js │ │ ├── oauth.js │ │ ├── persist-session.js │ │ ├── rate-limit.js │ │ └── user.js │ ├── helper.js │ ├── history-tracker.js │ ├── init-params.js │ ├── main.js │ ├── navigation │ │ ├── dialogs │ │ │ ├── select-skeleton-dialog.html │ │ │ └── select-skeleton-dialog.js │ │ ├── file-navigator.html │ │ ├── file-navigator.js │ │ ├── file-node.html │ │ ├── file-node.js │ │ ├── gist-info.html │ │ ├── gist-info.js │ │ ├── quick-start.html │ │ ├── quick-start.js │ │ └── quick-start.scss │ ├── notifications.html │ ├── notifications.js │ ├── notifications.scss │ ├── panel-resizer.html │ ├── panel-resizer.js │ ├── remove-expired-session.js │ ├── resources │ │ ├── dialog-close.html │ │ ├── dialog-close.js │ │ ├── file-icon.js │ │ ├── hidden-submit.html │ │ ├── hidden-submit.scss │ │ ├── index.js │ │ ├── left-click.js │ │ └── show-local-date.js │ ├── session-id.js │ ├── skeletons │ │ ├── aurelia.js │ │ ├── aurelia2.js │ │ ├── backbone.js │ │ ├── inferno.js │ │ ├── none.js │ │ ├── preact.js │ │ ├── react.js │ │ ├── skeleton-generator.js │ │ ├── svelte.js │ │ └── vue.js │ ├── top-bar │ │ ├── dialogs │ │ │ ├── confirm-draft-dialog.html │ │ │ ├── confirm-draft-dialog.js │ │ │ ├── confirm-fork-dialog.html │ │ │ ├── confirm-fork-dialog.js │ │ │ ├── confirm-open-dialog.html │ │ │ ├── confirm-open-dialog.js │ │ │ ├── confirm-share-dialog.html │ │ │ ├── confirm-share-dialog.js │ │ │ ├── open-gist-dialog.html │ │ │ ├── open-gist-dialog.js │ │ │ ├── share-gist-dialog.html │ │ │ ├── share-gist-dialog.js │ │ │ ├── short-cuts-dialog.html │ │ │ └── short-cuts-dialog.js │ │ ├── gist-bar.html │ │ ├── gist-bar.js │ │ ├── github-account.html │ │ └── github-account.js │ ├── url-handler.js │ └── worker-service.js ├── test-worker │ ├── au1-deps-finder.spec.js │ ├── cache-primitives.helper.js │ ├── cache-primitives.jsdelivr.spec.js │ ├── cache-primitives.local-cache.spec.js │ ├── cache-primitives.remote-cache.spec.js │ ├── deps-resolver.spec.js │ ├── dumber-cache.spec.js │ ├── dumber-session.spec.js │ ├── jsdelivr.spec.js │ ├── mock.js │ ├── setup.js │ ├── transpiler.spec.js │ ├── transpilers │ │ ├── au2.spec.js │ │ ├── js.spec.js │ │ ├── less.spec.js │ │ ├── sass.spec.js │ │ ├── svelte.spec.js │ │ └── text.spec.js │ └── turbo-resolver │ │ ├── registries │ │ └── npm-http.spec.js │ │ └── resolver.spec.js └── test │ ├── dialogs │ └── fuzzy-filter.spec.js │ ├── edit │ ├── edit-session.create-file.spec.js │ ├── edit-session.delete-file.spec.js │ ├── edit-session.delete-folder.spec.js │ ├── edit-session.import-data.spec.js │ ├── edit-session.spec.js │ ├── edit-session.update-file.spec.js │ ├── edit-session.update-path.spec.js │ ├── file-tree.spec.js │ └── opened-files.spec.js │ ├── history-tracker.spec.js │ ├── session-id.spec.js │ ├── setup.js │ └── worker-service.spec.js ├── copy-to-server.sh ├── deploy.sh ├── development.md ├── jsconfig.json ├── nginx.dev.conf ├── nginx.prod.conf ├── package.json └── server ├── .eslintrc.json ├── README.md ├── dumber-cache └── app.js ├── github-oauth └── app.js ├── package.json └── request.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | defaults: 12 | run: 13 | shell: bash 14 | working-directory: ./client 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | - run: npm install 20 | - run: npm run build --if-present 21 | - run: xvfb-run -a npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | .DS_STORE 3 | Thumbs.db 4 | 5 | # Editors 6 | .idea 7 | .vscode 8 | 9 | # Dependencies 10 | node_modules 11 | package-lock.json 12 | pnpm-lock.yaml 13 | yarn.lock 14 | bun.lockb 15 | 16 | # Compiled files 17 | dist 18 | /client/index.html 19 | /client-service-worker/__dumber-gist-worker.js 20 | .nyc_output 21 | coverage 22 | 23 | # Phusion Passenger 24 | /server/dumber-cache/public 25 | /server/dumber-cache/tmp 26 | /server/github-oauth/tmp 27 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # for pnpm, use flat node_modules 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Chunpeng Huo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dumber Gist 2 | 3 |  4 | 5 | A lightweight online IDE to write JS SPA prototypes in your own GitHub gists. 6 | 7 | https://gist.dumber.app 8 | 9 | Wiki: https://github.com/dumberjs/dumber-gist/wiki 10 | 11 | ----- 12 | Dumber Gist is written in [Aurelia](https://aurelia.io), inspired by [gist-run](https://github.com/gist-run), runs [dumber](https://dumber.js.org) bundler purely in browser. 13 | 14 | More information is in [development.md](https://github.com/dumberjs/dumber-gist/blob/master/development.md). 15 | -------------------------------------------------------------------------------- /client-service-worker/README.md: -------------------------------------------------------------------------------- 1 | # Dumber gist service worker 2 | 3 | For `https://random-id.gist.dumber.app/`, `__boot-up-worker.html` is loaded by an invisible iframe to setup a service worker. 4 | 5 | Once service worker is in place, a user app `index.html` will be added to cache and then be served as entry of the embedded app in the second visible iframe under exactly same host name `https://random-id.gist.dumber.app/`. 6 | -------------------------------------------------------------------------------- /client-service-worker/__boot-up-worker.html: -------------------------------------------------------------------------------- 1 | 2 | 33 | -------------------------------------------------------------------------------- /client-service-worker/__remove-expired-worker.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | -------------------------------------------------------------------------------- /client-service-worker/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dumberjs/dumber-gist/08824a52b3b77fbfa534ed3f778a58f0677da60f/client-service-worker/favicon.ico -------------------------------------------------------------------------------- /client-service-worker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |Sorry, you are trying to view an embedded app (iframe) from a new tab/window for (1) a previously closed dumber-gist window, or (2) in Safari (which sandboxes all iframes).
9 |Dumber Gist runs purely in browser, the embedded app is "created" by a Service Worker, it doesn't exist in the back-end. This URL is randomly generated and never reused.
10 | 11 | 12 | -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/no-explicit-any": 0, 13 | "@typescript-eslint/no-empty-function": 0, 14 | "no-console": 0, 15 | "getter-return": 0, 16 | "no-prototype-builtins": 0 17 | }, 18 | "env": { 19 | "es2017": true, 20 | "browser": true, 21 | "node": true, 22 | "worker": true 23 | }, 24 | "globals": { 25 | "requirejs": true, 26 | "HOST_NAMES": true, 27 | "DUMBER_MODULE_LOADER_DIST": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/.htmlhintrc: -------------------------------------------------------------------------------- 1 | { 2 | "tagname-lowercase": true, 3 | "attr-lowercase": true, 4 | "attr-value-double-quotes": true, 5 | "attr-no-duplication": true, 6 | "doctype-first": false, 7 | "tag-pair": true, 8 | "spec-char-escape": true, 9 | "id-unique": true, 10 | "src-not-empty": true, 11 | "title-require": true 12 | } 13 | -------------------------------------------------------------------------------- /client/.npmrc: -------------------------------------------------------------------------------- 1 | # for pnpm, use flat node_modules 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /client/.sass-lint.yml: -------------------------------------------------------------------------------- 1 | files: 2 | include: 'src/**/*.scss' 3 | rules: 4 | property-sort-order: 0 5 | no-color-keywords: 0 6 | quotes: 0 7 | hex-notation: 0 8 | single-line-per-selector: 0 9 | no-important: 0 10 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Dumber gist client side 2 | 3 | App is in `src/`, worker (dumber bundler) is in `src-worker/`. 4 | 5 | Following scripts are in `package.json`. 6 | 7 | ## Dev build 8 | 9 | npm run build 10 | 11 | Continuously build in watch mode: 12 | 13 | npm start 14 | 15 | ## Production build 16 | 17 | npm run build:prod 18 | 19 | ## Unit tests in browser 20 | 21 | npm test 22 | 23 | ## Unit tests in nodejs 24 | 25 | ./nodejs-test test/a-test-file 26 | ./nodejs-test test-worker/a-test-file 27 | -------------------------------------------------------------------------------- /client/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dumberjs/dumber-gist/08824a52b3b77fbfa534ed3f778a58f0677da60f/client/favicon.ico -------------------------------------------------------------------------------- /client/host-names.js: -------------------------------------------------------------------------------- 1 | const domainSubfix = process.env.NODE_ENV === 'production' ? 'app' : 'local'; 2 | const DUMBER_DOMAIN = process.env.DUMBER_DOMAIN || `dumber.${domainSubfix}`; 3 | const JSDELIVR_CDN_DOMAIN = process.env.JSDELIVR_CDN_DOMAIN || 'cdn.jsdelivr.net'; 4 | const JSDELIVR_DATA_DOMAIN = process.env.JSDELIVR_DATA_DOMAIN || 'data.jsdelivr.com'; 5 | 6 | module.exports = { 7 | domain : DUMBER_DOMAIN, 8 | host: `gist.${DUMBER_DOMAIN}`, 9 | clientUrl: `https://gist.${DUMBER_DOMAIN}`, 10 | cacheUrl: `https://cache.${DUMBER_DOMAIN}`, 11 | oauthUrl: `https://github-oauth.gist.${DUMBER_DOMAIN}`, 12 | jsdelivrDataDomain: JSDELIVR_DATA_DOMAIN, 13 | jsdelivrCdnDomain: JSDELIVR_CDN_DOMAIN 14 | }; 15 | -------------------------------------------------------------------------------- /client/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": false, 4 | "experimentalDecorators": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "skipLibCheck": true, 8 | "target": "ES2020", 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "importHelpers": true, 12 | "sourceMap": true, 13 | "allowJs": true, 14 | "baseUrl": "src" 15 | }, 16 | "include": [ 17 | "test", 18 | "src", 19 | "test-worker", 20 | "src-worker" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "dist", 25 | "scripts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dumber-gist", 3 | "description": "Dumber gist client side app", 4 | "keywords": [], 5 | "private": true, 6 | "license": "MIT", 7 | "devDependencies": { 8 | "@aurelia/plugin-conventions": "latest", 9 | "@typescript-eslint/eslint-plugin": "^7.8.0", 10 | "@typescript-eslint/parser": "^7.8.0", 11 | "aurelia-bootstrapper": "^2.4.1", 12 | "aurelia-combo": "^1.1.4", 13 | "aurelia-deps-finder": "^2.1.7", 14 | "aurelia-dialog-lite": "^1.0.1", 15 | "aurelia-testing": "^1.1.0", 16 | "base64-arraybuffer": "^1.0.2", 17 | "bcx-aurelia-dnd": "^1.6.0", 18 | "bcx-aurelia-reorderable-repeat": "^1.5.1", 19 | "bcx-validation": "^2.1.0", 20 | "browser-do": "^5.0.0", 21 | "clipboard": "^2.0.11", 22 | "codemirror": "^5.65.7", 23 | "cross-env": "^7.0.3", 24 | "eslint": "^8.56.0", 25 | "esm": "^3.2.25", 26 | "graphlib": "^2.1.8", 27 | "gulp": "^4.0.2", 28 | "gulp-dart-sass": "^1.1.0", 29 | "gulp-dumber": "^3.0.0", 30 | "gulp-if": "^3.0.0", 31 | "gulp-plumber": "^1.2.1", 32 | "gulp-swc": "^2.2.0", 33 | "gulp-terser": "^2.1.0", 34 | "htmlhint": "^1.1.4", 35 | "is-utf8": "^0.2.1", 36 | "less": "^4.2.1", 37 | "localforage": "^1.10.0", 38 | "lodash": "^4.17.21", 39 | "merge2": "^1.4.1", 40 | "moment": "^2.30.1", 41 | "nyc": "^15.1.0", 42 | "sass": "~1.49.11", 43 | "sass-lint": "^1.13.1", 44 | "sass.js": "^0.11.1", 45 | "semver": "^7.6.3", 46 | "source-map": "^0.8.0-beta.0", 47 | "svelte": "^5.16.0", 48 | "ts-plugin-inferno": "^6.1.0", 49 | "tslib": "^2.8.1", 50 | "typescript": "^5.7.2", 51 | "zora": "^6.0.0" 52 | }, 53 | "overrides": { 54 | "chokidar": "^3.5.3", 55 | "glob-stream": "^7.0.0", 56 | "glob-parent": "^6.0.2", 57 | "micromatch": "^4.0.5" 58 | }, 59 | "scripts": { 60 | "css-lint": "sass-lint -c .sass-lint.yml 'src/**/*.scss'", 61 | "js-lint": "eslint src test src-worker test-worker", 62 | "html-lint": "htmlhint -c .htmlhintrc src", 63 | "lint": "npm run js-lint && npm run css-lint && npm run html-lint", 64 | "coverage": "npm run test && nyc report --reporter=lcov --reporter=text", 65 | "start": "cross-env NODE_OPTIONS=--max_old_space_size=3584 gulp", 66 | "build": "cross-env NODE_OPTIONS=--max_old_space_size=3584 gulp build", 67 | "build:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max_old_space_size=3584 gulp build", 68 | "clear-cache": "gulp clear-cache", 69 | "pretest": "npm run lint && npm run build:test", 70 | "build:test": "cross-env NODE_ENV=test NODE_OPTIONS=--max_old_space_size=3584 gulp build", 71 | "test": "npm run test:app && npm run test:worker", 72 | "test:app": "browser-do --tap < dist/entry-bundle.js", 73 | "browser-test:app": "npm run build:test && browser-do --tap --browser chrome < dist/entry-bundle.js", 74 | "test:worker": "browser-do --tap < dist/bundler-worker.js", 75 | "browser-test:worker": "npm run build:test && browser-do --tap --browser chrome < dist/bundler-worker.js" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /client/src-worker/au1-deps-finder.js: -------------------------------------------------------------------------------- 1 | import {inject} from 'aurelia-dependency-injection'; 2 | import findDeps from 'aurelia-deps-finder'; 3 | import { 4 | JSDELIVR_PREFIX, 5 | CachePrimitives 6 | } from './cache-primitives'; 7 | 8 | @inject(CachePrimitives) 9 | export class Au1DepsFinder { 10 | constructor(primitives) { 11 | this.primitives = primitives; 12 | this.findDeps = this.findDeps.bind(this); 13 | this.readFile = this.readFile.bind(this); 14 | } 15 | 16 | async readFile(filename) { 17 | let packageWithVersion; 18 | let filePath; 19 | if (filename.startsWith(JSDELIVR_PREFIX)) { 20 | const part = filename.slice(JSDELIVR_PREFIX.length); 21 | let idx = part.indexOf('/'); 22 | if (idx > 0) { 23 | // For @scoped/npm-package/file/path.js 24 | if (part.startsWith('@')) idx = part.indexOf('/', idx + 1); 25 | if (idx > 0) { 26 | packageWithVersion = part.slice(0, idx); 27 | filePath = part.slice(idx + 1); 28 | } 29 | } 30 | } 31 | 32 | if (!packageWithVersion) { 33 | throw new Error('do not care'); 34 | } 35 | 36 | if (! await this.primitives.doesJsdelivrFileExist(packageWithVersion, filePath)) { 37 | throw new Error('File does not exist'); 38 | } 39 | } 40 | 41 | findDeps(filename, contents) { 42 | return findDeps(filename, contents, {readFile: this.readFile}); 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /client/src-worker/dumber-cache.js: -------------------------------------------------------------------------------- 1 | import {inject} from 'aurelia-dependency-injection'; 2 | import {CachePrimitives} from './cache-primitives'; 3 | 4 | function globalPostMessage() { 5 | return postMessage.apply(global, arguments); 6 | } 7 | 8 | @inject(CachePrimitives) 9 | export class DumberCache { 10 | constructor(primitives, _postMessage) { 11 | this.primitives = primitives; 12 | this._postMessage = _postMessage || globalPostMessage; 13 | this.getCache = this.getCache.bind(this); 14 | this.setCache = this.setCache.bind(this); 15 | this.clearCache = this.clearCache.bind(this); 16 | } 17 | 18 | async getCache(hash, meta) { 19 | try { 20 | return await this.primitives.getLocalCache(hash); 21 | } catch (e) { 22 | this._postMessage({type:'miss-cache', meta}); 23 | throw e; 24 | } 25 | } 26 | 27 | async setCache(hash, object) { 28 | try { 29 | await this.primitives.setLocalCache(hash, object); 30 | } catch (e) { 31 | // ignore 32 | } 33 | 34 | if (object.packageName) { 35 | // Globally share traced result for npm packages 36 | try { 37 | // Note this is noop for user not signed in. 38 | await this.primitives.setRemoteCache(hash, object); 39 | } catch (e) { 40 | // ignore 41 | } 42 | } 43 | } 44 | 45 | // To satisfy dumber 46 | async clearCache() {} 47 | } 48 | -------------------------------------------------------------------------------- /client/src-worker/index.js: -------------------------------------------------------------------------------- 1 | // metadata Polyfill from Aurelia 1 2 | import 'aurelia-polyfills'; 3 | // initializeTC39Metadata from Aurelia 2 4 | import '@aurelia/kernel'; 5 | 6 | import {DumberSession} from './dumber-session'; 7 | import {Container} from 'aurelia-dependency-injection'; 8 | 9 | (function patchConsole() { 10 | function patch(method) { 11 | const old = console[method]; 12 | console[method] = function() { 13 | const args = Array.prototype.slice.call(arguments, 0); 14 | if ( 15 | typeof args[0] === 'string' && 16 | args[0].startsWith('[dumber] ') 17 | ) { 18 | postMessage({ 19 | type: 'dumber-console', 20 | method: method, 21 | args: args.map((a, i) => { 22 | // Remove the leading '[dumber] ' 23 | if (i === 0) return a.slice(9); 24 | return a && a.toString ? a.toString() : a; 25 | }) 26 | }); 27 | } 28 | if (old) return old.apply(console, arguments); 29 | }; 30 | } 31 | 32 | const methods = ['log', 'error', 'warn', 'dir', 'debug', 'info', 'trace']; 33 | methods.forEach(m => patch(m)); 34 | })(); 35 | 36 | const container = new Container(); 37 | const session = container.get(DumberSession); 38 | 39 | onmessage = async function(event) { 40 | var action = event.data; 41 | const {id, type} = action; 42 | if (!type) return; 43 | 44 | try { 45 | let data; 46 | 47 | if (type === 'bundle') { 48 | data = await session.bundle(action.files); 49 | } else if (type === 'update-token') { 50 | // sync github token from main window 51 | global.__github_token = action.token; 52 | } else { 53 | throw new Error(`Unknown action: ${JSON.stringify(action)}`); 54 | } 55 | 56 | postMessage({type: 'ack', id, data}); 57 | } catch (e) { 58 | console.error('[dumber] ' + e.message); 59 | postMessage({type: 'err', id, error: e.message}); 60 | } 61 | }; 62 | 63 | postMessage({type: 'bundler-worker-up'}); 64 | -------------------------------------------------------------------------------- /client/src-worker/jsdelivr.js: -------------------------------------------------------------------------------- 1 | // Copied from dumber, enhanced with local and remote cache 2 | import {inject} from 'aurelia-dependency-injection'; 3 | import {JSDELIVR_PREFIX, CachePrimitives} from './cache-primitives'; 4 | 5 | @inject(CachePrimitives) 6 | export class Jsdelivr { 7 | constructor(primitives) { 8 | this.primitives = primitives; 9 | this.create = this.create.bind(this); 10 | } 11 | 12 | // use jsdelivr to find npm package files 13 | async create(packageConfig) { 14 | const name = packageConfig.name; 15 | let version = packageConfig.version; 16 | const dumberForcedMain = packageConfig.main; 17 | 18 | let packagePath; 19 | if (packageConfig.location) { 20 | const m = packageConfig.location.match(/^(.+)@(\d[^@]*)$/); 21 | if (m) { 22 | packagePath = m[1]; 23 | version = m[2]; 24 | } else { 25 | packagePath = packageConfig.location; 26 | } 27 | } else { 28 | packagePath = name; 29 | } 30 | 31 | if (version) { 32 | packagePath += '@' + version; 33 | } 34 | 35 | const packageJson = JSON.parse( 36 | (await this.primitives.getJsdelivrFile(packagePath, 'package.json')).contents 37 | ); 38 | 39 | if (!version) { 40 | // fillup version 41 | version = packageJson.version; 42 | packagePath += '@' + version; 43 | } else if (version !== packageJson.version) { 44 | packagePath = packagePath.slice(0, -version.length) + packageJson.version; 45 | version = packageJson.version; 46 | } 47 | 48 | const exists = async filePath => { 49 | if (filePath.startsWith('./')) filePath = filePath.slice(2); 50 | return this.primitives.doesJsdelivrFileExist(packagePath, filePath); 51 | }; 52 | 53 | const fileReader = async filePath => { 54 | if (filePath.startsWith('./')) filePath = filePath.slice(2); 55 | 56 | // Patch package.json with name and forced main 57 | if (filePath === 'package.json') { 58 | const meta = JSON.parse(JSON.stringify(packageJson)); 59 | if (meta.name !== name) { 60 | meta.name = name; 61 | } 62 | if (dumberForcedMain) { 63 | meta.dumberForcedMain = dumberForcedMain; 64 | } 65 | return { 66 | path: JSDELIVR_PREFIX + packagePath + '/' + filePath, 67 | contents: JSON.stringify(meta) 68 | }; 69 | } 70 | 71 | if (! await exists(filePath)) { 72 | throw new Error('no file "' + filePath + '" in ' + packagePath); 73 | } 74 | 75 | return this.primitives.getNpmPackageFile(packagePath, filePath); 76 | }; 77 | 78 | fileReader.packageConfig = packageConfig; 79 | fileReader.exists = exists; 80 | return fileReader; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /client/src-worker/transpiler.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import {inject} from 'aurelia-dependency-injection'; 3 | import {SvelteTranspiler} from './transpilers/svelte'; 4 | import {Au2Transpiler} from './transpilers/au2'; 5 | import {JsTranspiler} from './transpilers/js'; 6 | import {SassTranspiler} from './transpilers/sass'; 7 | import {LessTranspiler} from './transpilers/less'; 8 | import {TextTranspiler} from './transpilers/text'; 9 | 10 | @inject( 11 | SvelteTranspiler, 12 | Au2Transpiler, 13 | JsTranspiler, 14 | SassTranspiler, 15 | LessTranspiler, 16 | TextTranspiler 17 | ) 18 | export class Transpiler { 19 | constructor(...transpilers) { 20 | this.transpilers = transpilers; 21 | } 22 | 23 | findTranspiler(file, files) { 24 | return this.transpilers.find(t => t.match(file, files)); 25 | } 26 | 27 | async transpile(file, files, opts) { 28 | const transpiler = this.findTranspiler(file, files); 29 | let result; 30 | 31 | if (transpiler) { 32 | result = await transpiler.transpile(file, files, opts); 33 | } 34 | 35 | if (result) { 36 | let moduleId = path.relative('src', result.filename); 37 | return {...result, moduleId}; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/src-worker/transpilers/au2.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import {JsTranspiler} from './js'; 3 | import _ from 'lodash'; 4 | 5 | const EXTS = ['.html', '.js', '.ts']; 6 | 7 | export class Au2Transpiler { 8 | constructor() { 9 | this.jsTranspiler = new JsTranspiler(); 10 | } 11 | 12 | match(file, files) { 13 | const ext = path.extname(file.filename); 14 | if (!EXTS.includes(ext)) return false; 15 | 16 | const packageJson = _.find(files, {filename: 'package.json'}); 17 | if (packageJson) { 18 | try { 19 | const meta = JSON.parse(packageJson.content); 20 | // package "aurelia" is for aurelia 2 21 | return _.has(meta, 'dependencies.aurelia'); 22 | } catch (e) { 23 | // ignore 24 | } 25 | } 26 | } 27 | 28 | _lazyLoad() { 29 | if (!this._promise) { 30 | this._promise = import('@aurelia/plugin-conventions'); 31 | } 32 | 33 | return this._promise; 34 | } 35 | 36 | async transpile(file, files) { 37 | if (!this.match(file, files)) throw new Error('Cannot use Au2Transpiler for file: ' + file.filename); 38 | 39 | const au2 = await this._lazyLoad(); 40 | 41 | const au2Options = au2.preprocessOptions({ 42 | useProcessedFilePairFilename: true, 43 | stringModuleWrap: id => `text!${id}`, 44 | hmr: false 45 | }); 46 | 47 | const result = au2.preprocess( 48 | { 49 | path: file.filename, 50 | contents: file.content 51 | }, 52 | au2Options, 53 | (unit, filePath) => { 54 | let resolved = path.resolve(path.dirname(unit.path), filePath); 55 | // in browser env, path.resolve('src', './app.html') yields '/src/app.html' 56 | // Remove leading "/" 57 | resolved = resolved.substring(1); 58 | return !!_.find(files, {filename: resolved}); 59 | }, 60 | (unit, filePath) => { 61 | let resolved = path.resolve(path.dirname(unit.path), filePath); 62 | // in browser env, path.resolve('src', './app.html') yields '/src/app.html' 63 | // Remove leading "/" 64 | resolved = resolved.substring(1); 65 | const file = _.find(files, {filename: resolved}); 66 | if (file) { 67 | return file.content; 68 | } 69 | return ""; 70 | }, 71 | ); 72 | 73 | if (result) { 74 | const ext = path.extname(file.filename); 75 | const newFilename = file.filename + (ext === '.html' ? '.js': ''); 76 | 77 | return this.jsTranspiler.transpile( 78 | // ignore result.map for now 79 | {filename: newFilename, content: result.code}, 80 | files 81 | ); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /client/src-worker/transpilers/js.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import _ from 'lodash'; 3 | import transformInferno from 'ts-plugin-inferno'; 4 | import * as ts from 'typescript'; 5 | import {stripSourceMappingUrl} from 'dumber/lib/shared.js'; 6 | const EXTS = ['.js', '.ts', '.jsx', '.tsx']; 7 | 8 | function isAurelia1(files) { 9 | const packageJson = _.find(files, {filename: 'package.json'}); 10 | if (packageJson) { 11 | try { 12 | const meta = JSON.parse(packageJson.content); 13 | // package "aurelia" is for aurelia 2 14 | return _.has(meta, 'dependencies["aurelia-bootstrapper"]'); 15 | } catch (e) { 16 | // ignore 17 | } 18 | } 19 | } 20 | 21 | export class JsTranspiler { 22 | match(file) { 23 | const ext = path.extname(file.filename); 24 | return EXTS.includes(ext); 25 | } 26 | 27 | async transpile(file, files, opts = {}) { 28 | if (!this.match(file, files)) throw new Error('Cannot use JsTranspiler for file: ' + filename); 29 | 30 | const {filename, content} = file; 31 | const ext = path.extname(filename); 32 | 33 | const jsxPragma = opts.jsxPragma || 'React.createElement'; 34 | const jsxFrag = opts.jsxFrag || 'React.Fragment'; 35 | // Only au1 uses legacy decorators. 36 | const au1 = isAurelia1(files); 37 | 38 | const options = { 39 | fileName: filename, 40 | compilerOptions: { 41 | allowJs: true, 42 | checkJs: false, 43 | experimentalDecorators: au1, 44 | emitDecoratorMetadata: au1, 45 | inlineSources: true, 46 | // Don't compile to ModuleKind.AMD because 47 | // dumber can stub some commonjs globals. 48 | // The stubbing only applies to commonjs or ESM code. 49 | // Use ESNext so that dumber can normalise import (by babel), 50 | // so that we don't need esModuleInterop in tsc. 51 | module: ts.ModuleKind.ESNext, 52 | target: ts.ScriptTarget.ES2020, 53 | sourceMap: true 54 | } 55 | }; 56 | 57 | if (jsxPragma.startsWith('Inferno')) { 58 | // We didn't use jsxPragma and jsxFrag for Inferno. 59 | // ts-transform-inferno does all the work. 60 | options.transformers = { 61 | before: [transformInferno()] 62 | } 63 | } else { 64 | options.compilerOptions.jsx = ts.JsxEmit.React; 65 | options.compilerOptions.jsxFactory = jsxPragma; 66 | options.compilerOptions.jsxFragmentFactory = jsxFrag; 67 | } 68 | 69 | const result = ts.transpileModule(content, options); 70 | 71 | const {outputText, sourceMapText} = result; 72 | const newFilename = filename.slice(0, -ext.length) + '.js'; 73 | let newContent = stripSourceMappingUrl(outputText); 74 | if (!newContent) { 75 | // For ts type definition file, make a empty es module 76 | newContent = 'exports.__esModule = true;\n'; 77 | } 78 | 79 | const sourceMap = JSON.parse(sourceMapText); 80 | sourceMap.file = newFilename; 81 | sourceMap.sources = [filename]; 82 | sourceMap.sourceRoot = ''; 83 | 84 | return { 85 | filename: newFilename, 86 | content: newContent, 87 | sourceMap 88 | }; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /client/src-worker/transpilers/less.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import _ from 'lodash'; 3 | 4 | export class LessTranspiler { 5 | match(file) { 6 | const ext = path.extname(file.filename); 7 | return ext === '.less'; 8 | } 9 | 10 | _lazyLoad() { 11 | if (!this._promise) { 12 | this._promise = Promise.all([ 13 | import('less/lib/less'), 14 | import('less/lib/less/environment/abstract-file-manager'), 15 | import('less/lib/less-browser/plugin-loader') 16 | ]).then(results => results.map(r => r.default)) 17 | .then(results => { 18 | const [ createLess, 19 | AbstractFileManager, 20 | PluginLoader ] = results; 21 | 22 | class FileManager extends AbstractFileManager { 23 | files = {}; 24 | 25 | resetFiles() { 26 | this.files = {}; 27 | } 28 | 29 | setFiles(files) { 30 | this.files = files; 31 | } 32 | 33 | alwaysMakePathsAbsolute() { 34 | return false; // test true/false 35 | } 36 | 37 | supports() { 38 | return true; 39 | } 40 | 41 | loadFile(filename, currentDirectory, options) { 42 | if (currentDirectory && !this.isPathAbsolute(filename)) { 43 | filename = currentDirectory + filename; 44 | } 45 | 46 | filename = options.ext ? this.tryAppendExtension(filename, options.ext) : filename; 47 | 48 | return new Promise((resolve, reject) => { 49 | if (this.files[filename]) { 50 | resolve({filename, contents: this.files[filename]}); 51 | } else { 52 | reject({filename, message: `less.js file manager cannot find file "${filename}"`}); 53 | } 54 | }); 55 | } 56 | } 57 | 58 | const fileManager = new FileManager(); 59 | const less = createLess(null, [fileManager]); 60 | less.PluginLoader = PluginLoader; 61 | return {less, fileManager}; 62 | }); 63 | } 64 | 65 | return this._promise; 66 | } 67 | 68 | async transpile(file, files) { 69 | const {filename, content} = file; 70 | if (!this.match(file)) throw new Error('Cannot use LessTranspiler for file: ' + filename); 71 | 72 | const {less, fileManager} = await this._lazyLoad(); 73 | 74 | const ext = path.extname(filename); 75 | 76 | const cssFiles = {}; 77 | _.each(files, f => { 78 | const ext = path.extname(f.filename); 79 | if (ext === '.less' || ext === '.css') { 80 | const relative = path.relative(path.dirname(filename), f.filename); 81 | cssFiles[relative] = f.content; 82 | } 83 | }); 84 | 85 | fileManager.setFiles(cssFiles); 86 | 87 | const result = await less.render(content, { 88 | // TODO find a way to enable source map in browser. 89 | // The default less environment did not implement 90 | // getSourceMapGenerator. 91 | // The default less-browser env (we did not use) did 92 | // not implement it either. 93 | // It's possible to turn on, but less uses source-map 94 | // version 0.6.x, not latested 0.7.x that dumber uses. 95 | 96 | // sourceMap: { 97 | // outputSourceFiles: true 98 | // } 99 | }); 100 | 101 | const {css} = result; 102 | const newFilename = filename.slice(0, -ext.length) + '.css'; 103 | // const sourceMap = JSON.parse(map); 104 | // sourceMap.file = newFilename; 105 | 106 | return { 107 | filename: newFilename, 108 | content: css 109 | // sourceMap: sourceMap 110 | }; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /client/src-worker/transpilers/svelte.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import {JsTranspiler} from './js'; 3 | import {LessTranspiler} from './less'; 4 | import {SassTranspiler} from './sass'; 5 | 6 | export class SvelteTranspiler { 7 | constructor() { 8 | this.jsTranspiler = new JsTranspiler(); 9 | this.sassTranspiler = new SassTranspiler(); 10 | this.lessTranspiler = new LessTranspiler(); 11 | 12 | this.transpileCss = this.transpileCss.bind(this); 13 | this.transpileJs = this.transpileJs.bind(this); 14 | } 15 | 16 | match(file) { 17 | const ext = path.extname(file.filename); 18 | return ext === '.svelte'; 19 | } 20 | 21 | _lazyLoad() { 22 | if (!this._promise) { 23 | this._promise = import('svelte/compiler'); 24 | } 25 | 26 | return this._promise; 27 | } 28 | 29 | async transpileCss({content, attributes, filename}, files) { 30 | let ext = '.css'; 31 | if (attributes.lang === 'scss' || (attributes.type && attributes.type.startsWith('text/scss'))) { 32 | ext = '.scss'; 33 | } else if (attributes.lang === 'sass' || (attributes.type && attributes.type.startsWith('text/sass'))) { 34 | ext = '.sass'; 35 | } else if (attributes.lang === 'less' || (attributes.type && attributes.type.startsWith('text/less'))) { 36 | ext = '.less'; 37 | } 38 | 39 | const file = {filename: filename + ext, content}; 40 | if (ext === '.scss' || ext === '.sass') { 41 | const result = await this.sassTranspiler.transpile( 42 | file, 43 | [...files, file] 44 | ); 45 | return { 46 | code: result.content, 47 | map: result.sourceMap 48 | } 49 | } else if (ext === '.less') { 50 | const result = await this.lessTranspiler.transpile( 51 | file, 52 | [...files, file] 53 | ); 54 | return { 55 | code: result.content, 56 | map: result.sourceMap 57 | } 58 | } else { // css pass through 59 | return {code: content}; 60 | } 61 | } 62 | 63 | async transpileJs({content, filename}) { 64 | const result = await this.jsTranspiler.transpile( 65 | // Just go through typescript syntax for any js. 66 | {filename: filename + '.ts', content} 67 | ); 68 | return { 69 | code: result.content, 70 | map: result.sourceMap 71 | }; 72 | } 73 | 74 | async transpile(file, files) { 75 | if (!this.match(file)) throw new Error('Cannot use SvelteTranspiler for file: ' + file.filename); 76 | 77 | const {compile, preprocess} = await this._lazyLoad(); 78 | 79 | const {filename, content} = file; 80 | 81 | const newFilename = file.filename + '.js'; 82 | const preprocessed = await preprocess( 83 | content, 84 | { 85 | style: opts => this.transpileCss(opts, files), 86 | script: this.transpileJs 87 | }, 88 | {filename: filename} 89 | ); 90 | 91 | const compiled = compile(preprocessed.toString(), { 92 | outputFilename: newFilename 93 | }); 94 | 95 | let {code, map} = compiled.js; 96 | map.file = newFilename; 97 | map.sources = [filename]; 98 | map.sourceRoot = ''; 99 | 100 | return { 101 | filename: newFilename, 102 | content: code, 103 | sourceMap: map 104 | }; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /client/src-worker/transpilers/text.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | const EXTS = ['.html', '.css', '.svg', '.xml', '.json']; 4 | 5 | export class TextTranspiler { 6 | match(file) { 7 | const ext = path.extname(file.filename); 8 | return EXTS.indexOf(ext) !== -1; 9 | } 10 | 11 | async transpile(file) { 12 | if (!this.match(file)) throw new Error('Cannot use TextTranspiler for file: ' + file.filename); 13 | return file; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src-worker/turbo-resolver/registries/npm-http.js: -------------------------------------------------------------------------------- 1 | export class NpmHttpRegistry { 2 | constructor() { 3 | // Cannot use https://registry.npmjs.org which 4 | // doesn't have CORS enabled 5 | // this.registryUrl = 'https://registry.npmjs.cf'; 6 | // 2022-04-15 npmjs.cf has duplicated cors header issue. 7 | // npmjs.org seems works with CORS now. 8 | this.registryUrl = 'https://registry.npmjs.org'; 9 | this.cache = {}; 10 | this.fetching = {}; 11 | } 12 | 13 | resetCache() { 14 | this.cache = {}; 15 | } 16 | 17 | async fetch(name){ 18 | if(this.cache[name]) { 19 | return this.cache[name]; 20 | } 21 | 22 | if (!this.fetching[name]) { 23 | this.fetching[name] = fetch(`${this.registryUrl}/${name}`).then(async response => { 24 | if(!response.ok){ 25 | // npm can send a json error 26 | const dataError = (await (response.json().then(j => j && j.error).catch( () => null))); 27 | const error = `Could not load npm registry for ${name}: ${dataError || response.statusText}`; 28 | 29 | console.error(error); 30 | throw new Error(error); 31 | } 32 | 33 | return response.json(); 34 | }).then(json => { 35 | this.cache[name] = json; 36 | delete this.fetching[name]; 37 | return json; 38 | }); 39 | } 40 | 41 | return this.fetching[name]; 42 | } 43 | 44 | batchFetch(names){ 45 | return Promise.all(names.map(name => this.fetch(name))); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/src/_dialog.scss: -------------------------------------------------------------------------------- 1 | .dialog-lite-overlay { 2 | justify-content: flex-start; 3 | padding: 1rem 0; 4 | z-index: 1051; 5 | } 6 | 7 | .dialog { 8 | display: flex; 9 | flex-direction: column; 10 | box-shadow: 0 5px 15px $dark-shadow; 11 | border: 1px solid $gray-700; 12 | border-radius: $border-radius; 13 | padding: 3; 14 | width: 420px; 15 | max-height: 100%; 16 | color: $gray-100; 17 | border-image-source: initial; 18 | border-image-slice: initial; 19 | border-image-width: initial; 20 | border-image-outset: initial; 21 | border-image-repeat: initial; 22 | background-color: $gray-800; 23 | 24 | &.wide-dialog { 25 | @media (min-width: $small) { 26 | width: 600px; 27 | } 28 | } 29 | 30 | @media (max-width: ($tiny - .02)) { 31 | width: 310px; 32 | } 33 | 34 | > .dialog-header { 35 | display: flex; 36 | flex-wrap: nowrap; 37 | flex: 0 0 auto; 38 | padding: .75rem 1rem; 39 | font-size: 1.2rem; 40 | border-top-left-radius: $border-radius; 41 | border-top-right-radius: $border-radius; 42 | 43 | > .dialog-close { 44 | cursor: pointer; 45 | display: inline-block; 46 | margin: -.75rem -1rem -.75rem auto; 47 | margin-left: auto; 48 | color: $gray-400; 49 | font-size: 2.6rem; 50 | font-weight: 300; 51 | line-height: 1; 52 | width: 2.8rem; 53 | vertical-align: middle; 54 | text-align: center; 55 | border-radius: $border-radius; 56 | 57 | &:hover { 58 | color: $white; 59 | } 60 | } 61 | } 62 | 63 | > .dialog-body { 64 | display: block; 65 | flex: 0 1 auto; 66 | padding: .75rem 1rem; 67 | overflow-x: hidden; 68 | overflow-y: auto; 69 | -webkit-overflow-scrolling: touch; 70 | } 71 | 72 | .dialog-header + .dialog-body { 73 | padding-top: .25rem; 74 | } 75 | 76 | > .dialog-footer { 77 | display: flex; 78 | flex: 0 0 auto; 79 | padding: 1rem; 80 | background-color: $gray-900; 81 | text-align: right; 82 | border-bottom-left-radius: $border-radius; 83 | border-bottom-right-radius: $border-radius; 84 | 85 | .btn { 86 | min-width: 6rem; 87 | } 88 | 89 | .btn + .btn { 90 | margin-left: 10px; 91 | } 92 | 93 | button:disabled { 94 | cursor: default; 95 | opacity: .45; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /client/src/_gist-editor.scss: -------------------------------------------------------------------------------- 1 | .cm-s-gist-editor.CodeMirror { 2 | background: $gray-900; 3 | color: $gray-300; 4 | } 5 | 6 | .cm-s-gist-editor div.CodeMirror-selected { 7 | background: $gray-700; 8 | } 9 | 10 | .cm-s-gist-editor .CodeMirror-line::selection, 11 | .cm-s-gist-editor .CodeMirror-line > span::selection, 12 | .cm-s-gist-editor .CodeMirror-line > span > span::selection { 13 | background: rgba(56, 56, 56, 0.99); 14 | } 15 | 16 | .cm-s-gist-editor .CodeMirror-line::-moz-selection, 17 | .cm-s-gist-editor .CodeMirror-line > span::-moz-selection, 18 | .cm-s-gist-editor .CodeMirror-line > span > span::-moz-selection { 19 | background: rgba(56, 56, 56, 0.99); 20 | } 21 | 22 | .cm-s-gist-editor .CodeMirror-gutters { 23 | background: $gray-900; 24 | border-right: 2px dashed $gray-800; 25 | } 26 | 27 | .cm-s-gist-editor .CodeMirror-guttermarker { 28 | color: $gray-300; 29 | } 30 | 31 | .cm-s-gist-editor .CodeMirror-guttermarker-subtle { 32 | color: azure; 33 | } 34 | 35 | .cm-s-gist-editor .CodeMirror-linenumber { 36 | color: $gray-700; 37 | } 38 | 39 | .cm-s-gist-editor .CodeMirror-cursor { 40 | border-left: 2px solid $white; 41 | } 42 | 43 | .cm-s-gist-editor span.cm-keyword { 44 | color: $yellow-dark; 45 | font-weight: bold; 46 | } 47 | 48 | .cm-s-gist-editor span.cm-atom { 49 | color: #77F; 50 | } 51 | 52 | .cm-s-gist-editor span.cm-number { 53 | color: violet; 54 | } 55 | 56 | .cm-s-gist-editor span.cm-def { 57 | color: #fffabc; 58 | } 59 | 60 | .cm-s-gist-editor span.cm-variable { 61 | color: #abcdef; 62 | } 63 | 64 | .cm-s-gist-editor span.cm-variable-2 { 65 | color: #cacbcc; 66 | } 67 | 68 | .cm-s-gist-editor span.cm-variable-3, .cm-s-gist-editor span.cm-type { 69 | color: #def; 70 | } 71 | 72 | .cm-s-gist-editor span.cm-property { color: 73 | #fedcba; 74 | } 75 | 76 | .cm-s-gist-editor span.cm-operator { 77 | color: $yellow; 78 | } 79 | 80 | .cm-s-gist-editor span.cm-comment { 81 | color: #7a7b7c; font-style: italic; 82 | } 83 | 84 | .cm-s-gist-editor span.cm-string { 85 | color: $green; 86 | } 87 | 88 | .cm-s-gist-editor span.cm-meta { 89 | color: #C9F; 90 | } 91 | 92 | .cm-s-gist-editor span.cm-qualifier { 93 | color: $yellow; 94 | } 95 | 96 | .cm-s-gist-editor span.cm-builtin { 97 | color: #30aabc; 98 | } 99 | 100 | .cm-s-gist-editor span.cm-bracket { 101 | color: #8a8a8a; 102 | } 103 | 104 | .cm-s-gist-editor span.cm-tag { 105 | color: $yellow-dark; 106 | } 107 | 108 | .cm-s-gist-editor span.cm-attribute { 109 | color: $yellow; 110 | } 111 | 112 | .cm-s-gist-editor span.cm-error { 113 | color: #FF0000; 114 | } 115 | 116 | .cm-s-gist-editor span.cm-header { 117 | color: aquamarine; font-weight: bold; 118 | } 119 | 120 | .cm-s-gist-editor span.cm-link { 121 | color: blueviolet; 122 | } 123 | 124 | .cm-s-gist-editor .CodeMirror-activeline-background { 125 | background: #314151; 126 | } 127 | -------------------------------------------------------------------------------- /client/src/_variables.scss: -------------------------------------------------------------------------------- 1 | $charcoal: #2B2D33; 2 | 3 | $blue: #30A1C1; 4 | // $indigo: #6610f2; 5 | // $purple: #6f42c1; 6 | $pink: #D06566; 7 | $red: #DD6163; 8 | $orange: #D56D59; 9 | $yellow: #FEC624; 10 | $green: #34C371; 11 | // $teal: #20c997; 12 | $cyan: #5D99BD; 13 | 14 | 15 | $white: #fff; 16 | $gray-100: #f8f9fa; 17 | $gray-200: #e9ecef; 18 | $gray-300: #dee2e6; 19 | $gray-400: #ced4da; 20 | $gray-500: #adb5bd; 21 | $gray-600: #868e96; 22 | $gray-700: #495057; 23 | $gray-800: #343a40; 24 | $gray-900: #212529; 25 | $gray-1000: #101215; 26 | $black: #000; 27 | $shadow: transparentize($black, .95); 28 | $dark-shadow: transparentize($black, .7); 29 | $deep-dark-shadow: transparentize($black, .5); 30 | $bright-shadow: transparentize($white, .85); 31 | 32 | $red-light: lighten($red, 15%); 33 | $cyan-light: lighten($cyan, 38%); 34 | $yellow-light: lighten($yellow, 35%); 35 | $blue-light: lighten($blue, 45%); 36 | $green-light: lighten($green, 45%); 37 | 38 | $red-dark: darken($red, 20%); 39 | $cyan-dark: darken($cyan, 20%); 40 | $yellow-dark: darken($yellow, 20%); 41 | $blue-dark: darken($blue, 20%); 42 | $green-dark: darken($green, 20%); 43 | 44 | $tiny: 450px; 45 | $small: 800px; 46 | 47 | $border-radius: 3px; 48 | -------------------------------------------------------------------------------- /client/src/app.html: -------------------------------------------------------------------------------- 1 | 2 |
7 | Tracing new version of the npm packagespackage: ${missedCache.slice(0, 3).join(', ')} plus ${missedCache.length - 3} more ...
8 |
9 | It will be much faster after the cache was created.
10 |
16 | If you are using Brave browser, first try turning off Shields on current page.
17 | If it's other Chromium based browsers (e.g. Chrome, Edge, Brave), try turning on Allow sites to save and read cookie data (recommended) if you manually turned it off in Cookies and site data.
18 |
20 | If you customized Firefox Enhanced Tracking Protection, try NOT to set the cookies restriction to All third-party cookies or All cookies. 21 |
22 |23 | We are not sure about the possible Safari setting caused this. 24 |
25 |26 | Are you wondering why browser settings use term Cookies to mean Cookies, localStorage, Service Worker and maybe few more? We wonder that too, we guess they tried to make it easier to understand for the general users, but this is very confusing for the developers like us. 27 |
28 |29 | Please report a GitHub issue if you cannot resolve this. 30 |
31 |${error}36 |
Do you want to save them first before creating a new draft?
9 |Do you want to save them first before opening another GitHub Gist?
9 |Do you want to save them first before sharing this gist?
9 |\${name}
72 | `; 73 | const file = await jt.transpile({ 74 | filename: 'src/foo.html', 75 | content: code 76 | }, [au2PackageJson]); 77 | 78 | t.equal(file.filename, 'src/foo.html.js'); 79 | t.ok(file.content.includes('name = "foo"')); 80 | t.ok(file.content.includes('import "./foo.css"')); 81 | // t.notOk(file.content.includes("sourceMappingURL")); 82 | // t.equal(file.sourceMap.file, 'src/foo.html.js'); 83 | // t.deepEqual(file.sourceMap.sources, ['src/foo.html']); 84 | // t.deepEqual(file.sourceMap.sourcesContent, [code]); 85 | }); 86 | 87 | -------------------------------------------------------------------------------- /client/test-worker/transpilers/less.spec.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | import {LessTranspiler} from '../../src-worker/transpilers/less'; 3 | 4 | test('LessTranspiler matches less files', t => { 5 | const jt = new LessTranspiler(); 6 | t.ok(jt.match({filename: 'src/foo.less', content: ''})); 7 | }); 8 | 9 | test('LessTranspiler does not match other files', t => { 10 | const jt = new LessTranspiler(); 11 | t.notOk(jt.match({filename: 'src/foo.scss', content: ''})); 12 | t.notOk(jt.match({filename: 'src/foo.css', content: ''})); 13 | t.notOk(jt.match({filename: 'src/foo.html', content: ''})); 14 | }); 15 | 16 | test('LessTranspiler transpile less file', async t => { 17 | const jt = new LessTranspiler(); 18 | const code = '.a { .b { color: red; } }'; 19 | const f = { 20 | filename: 'src/foo.less', 21 | content: code 22 | }; 23 | const file = await jt.transpile(f, [f]); 24 | 25 | t.equal(file.filename, 'src/foo.css'); 26 | t.ok(file.content.includes('.a .b')); 27 | // t.equal(file.sourceMap.file, 'src/foo.css'); 28 | // t.deepEqual(file.sourceMap.sources, ['src/foo.less']); 29 | // t.deepEqual(file.sourceMap.sourcesContent, [code]); 30 | }); 31 | 32 | test('LessTranspiler transpile empty less file', async t => { 33 | const jt = new LessTranspiler(); 34 | const code = '\n\t\n'; 35 | const f = { 36 | filename: 'src/foo.less', 37 | content: code 38 | }; 39 | const file = await jt.transpile(f, [f]); 40 | 41 | t.equal(file.filename, 'src/foo.css'); 42 | t.equal(file.content, ''); 43 | t.notOk(file.sourceMap); 44 | }); 45 | 46 | test('LessTranspiler reject broken less file', async t => { 47 | const jt = new LessTranspiler(); 48 | const code = '.a {'; 49 | const f = { 50 | filename: 'src/foo.less', 51 | content: code 52 | }; 53 | try { 54 | await jt.transpile(f, [f]); 55 | t.fail('should not pass'); 56 | } catch (e) { 57 | t.ok(true, e.message); 58 | } 59 | }); 60 | 61 | test('LessTranspiler cannot tranpile other file', async t => { 62 | const jt = new LessTranspiler(); 63 | try { 64 | await jt.transpile({ 65 | filename: 'src/foo.js', 66 | content: '' 67 | }); 68 | t.fail('should not pass'); 69 | } catch (e) { 70 | t.ok(true, e.message); 71 | } 72 | }); 73 | 74 | test('LessTranspiler transpile less file with less import', async t => { 75 | const jt = new LessTranspiler(); 76 | const foo = '@import "variables";\n.a { .b { color: red; } }'; 77 | const variables = '.c { color: green }'; 78 | const f = { 79 | filename: 'src/foo.less', 80 | content: foo 81 | }; 82 | const f2 = { 83 | filename: 'src/variables.less', 84 | content: variables 85 | } 86 | const file = await jt.transpile(f, [f, f2]); 87 | 88 | t.equal(file.filename, 'src/foo.css'); 89 | t.ok(file.content.includes('.a .b')); 90 | t.ok(file.content.includes('.c')); 91 | // t.equal(file.sourceMap.file, 'src/foo.css'); 92 | // t.deepEqual(file.sourceMap.sources, ['src/foo.less', 'src/variables.less']); 93 | // t.deepEqual(file.sourceMap.sourcesContent, [foo, variables]); 94 | }); 95 | -------------------------------------------------------------------------------- /client/test-worker/transpilers/text.spec.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | import {TextTranspiler} from '../../src-worker/transpilers/text'; 3 | 4 | test('TextTranspiler matches html/css/svg/xml/json files', t => { 5 | const jt = new TextTranspiler(); 6 | t.ok(jt.match({filename: 'src/foo.html', content: ''})); 7 | t.ok(jt.match({filename: 'src/foo.css', content: ''})); 8 | t.ok(jt.match({filename: 'src/foo.svg', content: ''})); 9 | t.ok(jt.match({filename: 'src/foo.xml', content: ''})); 10 | t.ok(jt.match({filename: 'src/foo.json', content: ''})); 11 | }); 12 | 13 | test('TextTranspiler does not match other files', t => { 14 | const jt = new TextTranspiler(); 15 | t.notOk(jt.match({filename: 'src/foo.js', content: ''})); 16 | t.notOk(jt.match({filename: 'src/foo.jsx', content: ''})); 17 | t.notOk(jt.match({filename: 'src/foo.ts', content: ''})); 18 | t.notOk(jt.match({filename: 'src/foo.tsx', content: ''})); 19 | t.notOk(jt.match({filename: 'src/foo.scss', content: ''})); 20 | }); 21 | 22 | test('TextTranspiler passes through supported file', async t => { 23 | const jt = new TextTranspiler(); 24 | const code = 'lorem'; 25 | const file = await jt.transpile({ 26 | filename: 'src/foo.html', 27 | content: code 28 | }); 29 | 30 | t.equal(file.filename, 'src/foo.html'); 31 | t.equal(file.content, code); 32 | t.notOk(file.sourceMap); 33 | }); 34 | 35 | test('TextTranspiler cannot tranpile other file', async t => { 36 | const jt = new TextTranspiler(); 37 | try { 38 | await jt.transpile({ 39 | filename: 'src/foo.js', 40 | content: '' 41 | }); 42 | t.fail('should not pass'); 43 | } catch (e) { 44 | t.ok(true, e.message); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /client/test-worker/turbo-resolver/registries/npm-http.spec.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | import {NpmHttpRegistry} from '../../../src-worker/turbo-resolver/registries/npm-http'; 3 | 4 | const r = new NpmHttpRegistry(); 5 | 6 | test('NpmHttpRegistry fetches one package', async t => { 7 | const pack = await r.fetch('npm'); 8 | t.equal(pack.name, 'npm'); 9 | }); 10 | 11 | test('NpmHttpRegistry complains about unknown package', async t => { 12 | try { 13 | await r.fetch('@no/such/thing'); 14 | t.fail('should not pass'); 15 | } catch (e) { 16 | t.ok(true, e.message); 17 | } 18 | }); 19 | 20 | test('NpmHttpRegistry fetches multiple packages', async t => { 21 | const r = new NpmHttpRegistry(); 22 | const packs = await r.batchFetch(['npm', 'yarn']); 23 | t.deepEqual(packs.map(p => p.name), ['npm', 'yarn']); 24 | }); 25 | -------------------------------------------------------------------------------- /client/test-worker/turbo-resolver/resolver.spec.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | import _ from 'lodash'; 3 | import {NpmHttpRegistry} from '../../src-worker/turbo-resolver/registries/npm-http'; 4 | import {Resolver} from '../../src-worker/turbo-resolver/resolver'; 5 | 6 | test('Resolver resolves au1 deps', async t => { 7 | const r = new Resolver(new NpmHttpRegistry()); 8 | 9 | const result = await r.resolve({ 10 | 'aurelia-bootstrapper': '^2.0.0' 11 | }); 12 | 13 | t.deepEqual(Object.keys(result.appDependencies), ['aurelia-bootstrapper']); 14 | t.ok(_.some(Object.keys(result.resDependencies), k => k.startsWith('aurelia-framework'))); 15 | }); 16 | 17 | test('Resolver resolves vue2 deps', async t => { 18 | const r = new Resolver(new NpmHttpRegistry()); 19 | 20 | const result = await r.resolve({ 21 | 'vue': '^2.0.0' 22 | }); 23 | 24 | t.deepEqual(Object.keys(result.appDependencies), ['vue']); 25 | t.equal(Object.keys(result.resDependencies).length, 0); 26 | }); 27 | 28 | test('Resolver resolves invalid deps', async t => { 29 | const r = new Resolver(new NpmHttpRegistry()); 30 | try { 31 | await r.resolve({'an-invalid-module-name': 'latest'}); 32 | t.fail('should not pass'); 33 | } catch (e) { 34 | t.equal(e.message, 'Could not load npm registry for an-invalid-module-name: Not found'); 35 | } 36 | }); 37 | 38 | test('Resolver resolves invalid version', async t => { 39 | const r = new Resolver(new NpmHttpRegistry()); 40 | try { 41 | await r.resolve({'vue': '^2000.0.0'}); 42 | t.fail('should not pass'); 43 | } catch (e) { 44 | t.equal(e.message, 'npm package "vue" was not found with requested version: "^2000.0.0".'); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /client/test/dialogs/fuzzy-filter.spec.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | import {fuzzyFilter} from '../../src/dialogs/fuzzy-filter'; 3 | 4 | test('fuzzyFilter returns original list', t => { 5 | t.deepEqual( 6 | fuzzyFilter('', ['foo', 'bar', 'lo']), 7 | [['foo'], ['bar'], ['lo']] 8 | ); 9 | }); 10 | 11 | test('fuzzyFilter returns sorted filtered list', t => { 12 | t.deepEqual( 13 | fuzzyFilter('o', ['foo', 'bar', 'lo']), 14 | [['l', 'o'], ['f', 'o', 'o']] 15 | ); 16 | }); 17 | 18 | test('fuzzyFilter returns sorted filtered list, case2', t => { 19 | const list = ['package.json', 'src/main.js', 'src/app.html', 'src/app.js', 'index.html']; 20 | t.deepEqual( 21 | fuzzyFilter(' aj ', list), 22 | [ 23 | ['p', 'a', 'ckage.', 'j', 'son'], 24 | ['src/m', 'a', 'in.', 'j', 's'], 25 | ['src/', 'a', 'pp.', 'j', 's'] 26 | ] 27 | ); 28 | }); 29 | 30 | test('fuzzyFilter returns sorted filtered list, case3', t => { 31 | const list = ['package.json', 'src/main.js', 'src/app.html', 'src/app.js', 'index.html']; 32 | t.deepEqual( 33 | fuzzyFilter('smain', list), 34 | [ 35 | ['', 's', 'rc/', 'main', '.js'], 36 | ] 37 | ); 38 | }); 39 | 40 | test('fuzzyFilter returns sorted filtered list, case4', t => { 41 | const list = ['package.json', 'src/main.js', 'src/app.html', 'src/app.js', 'index.html']; 42 | t.deepEqual( 43 | fuzzyFilter('js', list), 44 | [ 45 | ['src/main.', 'js'], 46 | ['src/app.', 'js'], 47 | ['package.', 'js', 'on'] 48 | ] 49 | ); 50 | }); 51 | -------------------------------------------------------------------------------- /client/test/edit/edit-session.import-data.spec.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | import {EditSession} from '../../src/edit/edit-session'; 3 | 4 | function makeEa(published) { 5 | return { 6 | publish(event, data) { 7 | published.push([event, data]); 8 | } 9 | }; 10 | } 11 | 12 | function makeWorkerService(actions) { 13 | return { 14 | async perform(action) { 15 | actions.push(action); 16 | 17 | if (action.type === 'bundle') { 18 | return ['bundled-files']; 19 | } 20 | } 21 | }; 22 | } 23 | 24 | const consoleLog = { 25 | dumberLogs: { 26 | push() {} 27 | } 28 | } 29 | 30 | test('EditSession imports data', t => { 31 | const actions = []; 32 | const published = []; 33 | 34 | const ea = makeEa(published); 35 | const workerService = makeWorkerService(actions); 36 | const es = new EditSession(ea, workerService, consoleLog); 37 | 38 | const gist = { 39 | description: 'desc', 40 | files: [ 41 | { 42 | filename: 'src/main.js', 43 | content: 'main' 44 | }, 45 | { 46 | filename: 'index.html', 47 | content: 'index-html' 48 | }, 49 | { 50 | filename: 'package.json', 51 | content: '{"dependencies":{}}' 52 | } 53 | ] 54 | }; 55 | 56 | const newGist = { 57 | description: 'new-gist-desc', 58 | files: [ 59 | { 60 | filename: 'src/main.js', 61 | content: 'main2' 62 | } 63 | ] 64 | }; 65 | 66 | es.loadGist(gist); 67 | es.importData({ 68 | description: 'new-desc', 69 | files: [], 70 | gist: newGist 71 | }); 72 | es.mutationChanged(); 73 | t.deepEqual(published, [ 74 | ['loaded-gist', undefined], 75 | ['imported-data', undefined] 76 | ]); 77 | t.equal(es.description, 'new-desc'); 78 | t.equal(es.files.length, 0); 79 | t.equal(es.gist, newGist); 80 | t.notOk(es.isRendered); 81 | t.ok(es.isChanged); 82 | }); 83 | 84 | test('EditSession imports data with only files', t => { 85 | const actions = []; 86 | const published = []; 87 | 88 | const ea = makeEa(published); 89 | const workerService = makeWorkerService(actions); 90 | const es = new EditSession(ea, workerService, consoleLog); 91 | 92 | const gist = { 93 | description: 'desc', 94 | files: [ 95 | { 96 | filename: 'src/main.js', 97 | content: 'main' 98 | }, 99 | { 100 | filename: 'index.html', 101 | content: 'index-html' 102 | }, 103 | { 104 | filename: 'package.json', 105 | content: '{"dependencies":{}}' 106 | } 107 | ] 108 | }; 109 | 110 | es.loadGist(gist); 111 | es.importData({ 112 | files: [ 113 | { 114 | filename: 'foo.js', 115 | content: 'foo', 116 | isChanged: true 117 | }, 118 | { 119 | filename: 'index.html', 120 | content: 'html', 121 | isChanged: false 122 | } 123 | ] 124 | }); 125 | es.mutationChanged(); 126 | t.deepEqual(published, [ 127 | ['loaded-gist', undefined], 128 | ['imported-data', undefined] 129 | ]); 130 | t.equal(es.description, 'desc'); 131 | t.deepEqual(es.files, [ 132 | { 133 | filename: 'foo.js', 134 | content: 'foo', 135 | isChanged: true 136 | }, 137 | { 138 | filename: 'index.html', 139 | content: 'html', 140 | isChanged: false 141 | } 142 | ]); 143 | t.equal(es.gist, gist); 144 | t.notOk(es.isRendered); 145 | t.ok(es.isChanged); 146 | }); 147 | -------------------------------------------------------------------------------- /client/test/session-id.spec.js: -------------------------------------------------------------------------------- 1 | import {test} from 'zora'; 2 | import {SessionId} from '../src/session-id'; 3 | 4 | test('SessionId generates random sessionId', t => { 5 | const s = new SessionId({}); 6 | t.ok(s.id.match(/^[0-9a-f]{32}$/)); 7 | }); 8 | -------------------------------------------------------------------------------- /client/test/setup.js: -------------------------------------------------------------------------------- 1 | import 'aurelia-polyfills'; 2 | import {initialize} from 'aurelia-pal-browser'; 3 | initialize(); 4 | -------------------------------------------------------------------------------- /copy-to-server.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash --login 2 | 3 | # Copy js before html files 4 | scp client/dist/* huocp@a.gist.dumber.app:dumber-gist/client/dist/ 5 | scp client/favicon.ico huocp@a.gist.dumber.app:dumber-gist/client/ 6 | scp client/index.html huocp@a.gist.dumber.app:dumber-gist/client/ 7 | 8 | scp client-service-worker/__dumber-gist-worker.js huocp@a.gist.dumber.app:dumber-gist/client-service-worker/ 9 | scp client-service-worker/__boot-up-worker.html huocp@a.gist.dumber.app:dumber-gist/client-service-worker/ 10 | scp client-service-worker/__remove-expired-worker.html huocp@a.gist.dumber.app:dumber-gist/client-service-worker/ 11 | scp client-service-worker/favicon.ico huocp@a.gist.dumber.app:dumber-gist/client-service-worker/ 12 | scp client-service-worker/index.html huocp@a.gist.dumber.app:dumber-gist/client-service-worker/ 13 | 14 | scp server/dumber-cache/app.js huocp@a.gist.dumber.app:dumber-gist/server/dumber-cache/ 15 | scp server/github-oauth/app.js huocp@a.gist.dumber.app:dumber-gist/server/github-oauth/ 16 | scp server/request.js huocp@a.gist.dumber.app:dumber-gist/server/ 17 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash --login 2 | 3 | cd client 4 | npm run build:prod 5 | 6 | cd .. 7 | 8 | echo "Deploy files to Digital Ocean ..." 9 | 10 | ./copy-to-server.sh 11 | 12 | echo "Deployed!" 13 | echo "To restart cache or oauth server, ssh to dumber.app and touch dumber-gist/server/dumber-cache/tmp/restart.txt and dumber-gist/server/github-oauth/tmp/restart.txt" 14 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "baseUrl": "src" 5 | }, 6 | "exclude": [ 7 | "node_modules", 8 | "output", 9 | "dist", 10 | "scripts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dumber-gist", 3 | "private": true, 4 | "version": "0.19.0", 5 | "description": "The private is set to true to avoid publishing to npm", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "standard-changelog": "^6.0.0" 9 | }, 10 | "scripts": { 11 | "version": "standard-changelog && git add CHANGELOG.md", 12 | "postversion": "git push && git push --tags && ./deploy.sh" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "eslint:recommended", 4 | "parserOptions": { 5 | "ecmaVersion": 8 6 | }, 7 | "rules": { 8 | "no-console": 0, 9 | "getter-return": 0, 10 | "no-prototype-builtins": 0 11 | }, 12 | "env": { 13 | "es2017": true, 14 | "node": true 15 | }, 16 | "globals": { 17 | } 18 | } -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Dumber gist back-end support 2 | 3 | This back-end only does caching and GitHub OAuth. 4 | 5 | GitHub oauth server for dumber-gist 6 | 7 | ## Running in dev mode 8 | 9 | Use local nginx+passenger to start both apps. TODO guide. 10 | 11 | ## Or run them manually 12 | 13 | Start github-oauth server at port 500 14 | 15 | node github-oauth/app 16 | 17 | Start dumber-cache server at port 5001 18 | 19 | node dumber-cache/app 20 | 21 | -------------------------------------------------------------------------------- /server/dumber-cache/app.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const path = require('path'); 3 | const fs = require('fs').promises; 4 | const {receiveData, fetch} = require('../request'); 5 | 6 | const PORT = 5001; 7 | const CACHE_DIR = path.join(__dirname, 'public'); 8 | 9 | const domainSubfix = process.env.NODE_ENV === 'production' ? 'app' : 'local'; 10 | const DUMBER_DOMAIN = process.env.DUMBER_DOMAIN || `dumber.${domainSubfix}`; 11 | const HOST = `https://gist.${DUMBER_DOMAIN}`; 12 | const JSDELIVR_CDN_DOMAIN = process.env.JSDELIVR_CDN_DOMAIN || 'cdn.jsdelivr.net'; 13 | 14 | const knownTokens = {}; 15 | 16 | async function getUser(token) { 17 | if (!token) throw new Error('Invalid user'); 18 | if (knownTokens[token]) return knownTokens[token]; 19 | 20 | const {statusCode, body} = await fetch('https://api.github.com/user', { 21 | headers: { 22 | Authorization: `token ${token}`, 23 | 'User-Agent': `dumber-gist/0.0.1 (${HOST})` 24 | } 25 | }); 26 | 27 | if (statusCode !== 200) { 28 | throw new Error('Invalid user: ' + body); 29 | } 30 | 31 | try { 32 | const user = JSON.parse(body); 33 | knownTokens[token] = user.login; 34 | return user.login; 35 | } catch (e) { 36 | throw new Error('Invalid user: ' + e.message); 37 | } 38 | } 39 | 40 | function cachedFilePath(hash) { 41 | const folder = hash.slice(0, 2); 42 | const fileName = hash.slice(2); 43 | return path.resolve(CACHE_DIR, folder, fileName); 44 | } 45 | 46 | async function setCache(hash, object) { 47 | object.__dumber_hash = hash; 48 | const filePath = cachedFilePath(hash); 49 | await fs.mkdir(path.dirname(filePath), {recursive: true}); 50 | 51 | try { 52 | // x means fail if file already exists 53 | await fs.writeFile(filePath, JSON.stringify(object), {flag: 'wx'}); 54 | } catch (err) { 55 | if (err && !err.code === 'EEXIST') { 56 | console.error(`Failed to write cache ${filePath}: ${err.code} ${err.message}`); 57 | } 58 | return; 59 | } 60 | 61 | if (object.path.startsWith(`//${JSDELIVR_CDN_DOMAIN}/npm/`)) { 62 | // slice to "npm/..." 63 | const npmPath = path.resolve(CACHE_DIR, object.path.slice(19)); 64 | await fs.mkdir(path.dirname(npmPath), {recursive: true}); 65 | await fs.link(filePath, npmPath); 66 | } 67 | } 68 | 69 | // Note CORS headers are now added by nginx 70 | async function handleRequest(req, res) { 71 | const origin = req.headers.origin; 72 | res.setHeader('Content-Type', 'text/plain'); 73 | 74 | if (origin === HOST) { 75 | if (req.method === 'OPTIONS') { 76 | res.writeHead(200); 77 | res.end(); 78 | return; 79 | } 80 | 81 | // Only set-cache action is handled by nodejs + passenger. 82 | // get-cache is handled by nginx directly. 83 | if (req.method === 'POST') { 84 | try { 85 | const d = await receiveData(req) 86 | // const {token, hash, object} = d; 87 | const {hash, object} = d; 88 | if (!hash || !object) { 89 | throw new Error('In complete request'); 90 | } 91 | 92 | // Skip user check, allow anonymous contribution to cache 93 | // await getUser(token); 94 | setCache(hash, object); 95 | 96 | res.writeHead(200); 97 | res.end(); 98 | } catch (error) { 99 | res.writeHead(500); 100 | res.end(error.message, 'utf8'); 101 | } 102 | return; 103 | } 104 | } 105 | 106 | res.writeHead(404); 107 | res.end(`Not Found: ${req.method} ${req.url}\n`, 'utf8'); 108 | } 109 | 110 | console.log('Start dumber-gist cache server ...'); 111 | http.createServer(handleRequest).listen(PORT); 112 | -------------------------------------------------------------------------------- /server/github-oauth/app.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const {receiveData, fetch} = require('../request'); 3 | 4 | const PORT = 5000; 5 | const CLIENT_ID = process.env.DR_CLIENT_ID; 6 | const CLIENT_SECRET = process.env.DR_CLIENT_SECRET; 7 | 8 | const domainSubfix = process.env.NODE_ENV === 'production' ? 'app' : 'local'; 9 | const domain = process.env.DUMBER_DOMAIN || `dumber.${domainSubfix}`; 10 | const HOST = `https://gist.${domain}`; 11 | 12 | if (!CLIENT_ID || !CLIENT_SECRET) { 13 | throw new Error('no client_id or client_secret'); 14 | } 15 | 16 | // Note CORS headers are now added by nginx 17 | async function handleRequest(req, res) { 18 | const origin = req.headers.origin; 19 | res.setHeader('Content-Type', 'text/plain'); 20 | 21 | if (origin === HOST) { 22 | if (req.method === 'OPTIONS') { 23 | res.writeHead(200); 24 | res.end(); 25 | return; 26 | } 27 | 28 | if (req.method === 'POST' && /^\/access_token$/.test(req.url)) { 29 | try { 30 | const data = await receiveData(req); 31 | const params = { 32 | client_id: CLIENT_ID, 33 | client_secret: CLIENT_SECRET, 34 | code: data.code, 35 | redirect_uri: data.redirect_uri, 36 | state: data.state 37 | }; 38 | 39 | const result = await fetch('https://github.com/login/oauth/access_token', { 40 | method: 'POST', 41 | params 42 | }); 43 | res.writeHead(200, result.headers); 44 | res.end(result.body, 'utf8'); 45 | } catch(error) { 46 | res.writeHead(500); 47 | res.end(error.message, 'utf8'); 48 | } 49 | return; 50 | } 51 | } 52 | 53 | res.writeHead(404); 54 | res.end(`Not Found: ${req.method} ${req.url}\n`, 'utf8'); 55 | } 56 | 57 | console.log('Start dumber-gist github-oauth server ...'); 58 | http.createServer(handleRequest).listen(PORT); 59 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dumber-gist-server", 3 | "version": "0.1.0", 4 | "description": "GitHub OAuth server, and cache server for dumber-gist", 5 | "private": true, 6 | "main": "github-oauth.js", 7 | "scripts": { 8 | "lint": "eslint dumber-cache github-oauth request.js" 9 | }, 10 | "license": "MIT", 11 | "devDependencies": { 12 | "eslint": "^8.56.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/request.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | const querystring = require('querystring'); 3 | 4 | exports.receiveData = function(request) { 5 | return new Promise((resolve, reject) => { 6 | let body = ''; 7 | 8 | request.on('data', chunk => body += chunk); 9 | request.on('end', () => { 10 | try { 11 | resolve(JSON.parse(body)); 12 | } catch (e) { 13 | reject(e); 14 | } 15 | }); 16 | request.on('error', reject); 17 | }); 18 | }; 19 | 20 | exports.fetch = function(uri, opts = {}) { 21 | const options = { 22 | method: opts.method || 'GET', 23 | headers: {} 24 | }; 25 | 26 | if (opts.headers) { 27 | Object.assign(options.headers, opts.headers); 28 | } 29 | 30 | const form = querystring.stringify(opts.params || {}); 31 | if (options.method === 'POST') { 32 | Object.assign(options.headers, { 33 | 'Content-Type': 'application/x-www-form-urlencoded', 34 | 'Content-Length': form.length 35 | }); 36 | } 37 | 38 | return new Promise((resolve, reject) => { 39 | const req = https.request(uri, options, res => { 40 | res.setEncoding('utf8'); 41 | const { statusCode, headers } = res; 42 | let body = ''; 43 | 44 | res.on('data', chunk => body += chunk); 45 | res.on('end', () => resolve({ statusCode, headers, body })); 46 | res.on('error', reject); 47 | }); 48 | 49 | if (options.method === 'POST') { 50 | req.write(form); 51 | } 52 | req.end(); 53 | }); 54 | }; 55 | --------------------------------------------------------------------------------