├── index.js ├── examples └── express │ ├── README.md │ ├── package.json │ └── index.js ├── .brackets.json ├── .todo ├── .npmignore ├── .gitignore ├── .gitmodules ├── .travis.yml ├── docker └── dev │ ├── start.sh │ ├── README.md │ └── Dockerfile ├── lib ├── post-install.js ├── pre-publish.js ├── domains │ ├── socket.js │ ├── BaseDomain.js │ ├── Logger.js │ ├── ConnectionManager.js │ ├── DomainManager.js │ └── ExtensionManagerDomain.js ├── shim.js ├── file-sys │ └── native.js ├── files.js └── server.js ├── LICENSE ├── hacks ├── low-level-fs.js ├── app.js ├── .jshintrc └── NodeConnection.js ├── CHANGELOG.md ├── bin └── run.js ├── test ├── server_test.js └── .jshintrc ├── package.json ├── CONTRIBUTING.md ├── .jshintrc ├── README.md └── Gruntfile.js /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/server"); 2 | -------------------------------------------------------------------------------- /examples/express/README.md: -------------------------------------------------------------------------------- 1 | brackets-server-express 2 | ----------------------- 3 | 4 | Example for embedding Brackets Server in Express applications. 5 | -------------------------------------------------------------------------------- /.brackets.json: -------------------------------------------------------------------------------- 1 | { 2 | "brackets-file-tree-exclude.excludeList": [ 3 | "/.git/", 4 | "/.tmp/", 5 | "/bower_components/", 6 | "/node_modules/", 7 | "/brackets-dist/" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.todo: -------------------------------------------------------------------------------- 1 | { 2 | "search": { 3 | "scope": "project", 4 | "excludeFolders": ["node_modules", "brackets-src", "brackets-dist", "brackets-srv", "brackets", "new-project"], 5 | "excludeFiles": [".html"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | cache 2 | examples 3 | .grunt 4 | .jshintrc 5 | .travis.yml 6 | .todo 7 | test 8 | Gruntfile.js 9 | jsdoc.json 10 | CONTRIBUTING.md 11 | brackets/ 12 | brackets-src 13 | embedded-ext 14 | examples 15 | projects 16 | hacks 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | brackets/ 17 | brackets-dist 18 | brackets-srv 19 | projects 20 | -------------------------------------------------------------------------------- /examples/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brackets-server-express", 3 | "description": "Example for embedding Brackets Server in Express applications.", 4 | "version": "0.1.0", 5 | "dependencies": { 6 | "express": "*", 7 | "brackets": "*" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "brackets-src"] 2 | path = brackets-src 3 | url = https://github.com/adobe/brackets.git 4 | [submodule "embedded-ext/new-project"] 5 | path = embedded-ext/new-project 6 | url = https://github.com/JeffryBooher/brackets-newproject-extension.git 7 | [submodule "embedded-ext/client-fs"] 8 | path = embedded-ext/client-fs 9 | url = https://github.com/rabchev/brackets-remote-fs.git 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.11' 4 | - '0.10' 5 | before_install: npm install -g grunt-cli 6 | deploy: 7 | provider: npm 8 | email: boyan@rabchev.com 9 | api_key: 10 | secure: bWt1wLTheu3LjIgmGciF/7X0dZyThMMnWi8TYBpSGNEH0/XzJGsOMeoF1j8aEfhFuM655l10JmOx+jGpWIfxCkHd0uuzDdr9rPiNNRwa5NZkmRWUbv23CCAS26N5/manbiwhpBbZQjCWxusGw24cuxDBaBlGGDkY8uAmIr6rPfE= 11 | on: 12 | tags: true 13 | repo: rabchev/brackets-server 14 | node: 0.10 15 | branch: master 16 | -------------------------------------------------------------------------------- /docker/dev/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$DEBUG" ]; then 4 | echo "starting in normal mode" 5 | node "/var/brackets-server/bin/run" $* 6 | elif [ $DEBUG = "debug" ]; then 7 | echo "starting in debug mode" 8 | node-inspector & 9 | node --debug "/var/brackets-server/bin/run" $* 10 | elif [ $DEBUG = "debug-brk" ]; then 11 | echo "starting in debug-brk mode" 12 | node-inspector & 13 | node --debug-brk "/var/brackets-server/bin/run" $* 14 | else 15 | echo "unsupported debug variable" 16 | exit 1 17 | fi 18 | -------------------------------------------------------------------------------- /examples/express/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var path = require("path"), 4 | http = require("http"), 5 | express = require("express"), 6 | brackets = require("brackets"), 7 | app = express(), 8 | server = http.createServer(app); 9 | 10 | app.get("/", function (req, res) { 11 | res.send("Hello World"); 12 | }); 13 | 14 | var bracketsOpts = { 15 | projectsDir: path.join(__dirname, ".."), 16 | supportDir: path.join(__dirname, "..", "/support") 17 | }; 18 | brackets(server, bracketsOpts); 19 | 20 | server.listen(3000); 21 | 22 | console.log("Your application is availble at http://localhost:3000"); 23 | console.log("You can access Brackets on http://localhost:3000/brackets/"); 24 | -------------------------------------------------------------------------------- /lib/post-install.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"), 4 | path = require("path"), 5 | glob = require("glob"), 6 | opts = { 7 | cwd: path.join(__dirname, "..", "brackets-srv") 8 | }; 9 | 10 | glob("**/node_modules_", opts, function (err, files) { 11 | if (err) { 12 | throw err; 13 | } 14 | 15 | if (files) { 16 | files.sort(function (a, b) { 17 | return b.length - a.length; 18 | }); 19 | 20 | files.forEach(function (file) { 21 | file = path.join(opts.cwd, file); 22 | fs.renameSync(file, file.substr(0, file.length - 1)); 23 | console.log("file: " + file.substr(0, file.length - 1)); 24 | }); 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /docker/dev/README.md: -------------------------------------------------------------------------------- 1 | Docker Image for Brackets-Server Development 2 | ============================================ 3 | 4 | How to build 5 | ------------ 6 | 7 | $ docker build -t brackets_server_dev 8 | 9 | This will pull the last commit in master branch and build brackets-server. 10 | 11 | How to use this image 12 | --------------------- 13 | 14 | Start in normal mode: 15 | 16 | $ docker run -d -p 6800:6800 --name brackets-server -v /home/myname/Projects:/root/Projects brackets_server_dev 17 | 18 | Open browser and navigate to http://localhost:6800 19 | 20 | Start in debug mode: 21 | 22 | $ docker run -e DEBUG=debug -d -p 6800:6800 -p 8080:8080 --name brackets-server-debug brackets_server_dev 23 | 24 | The value of DEBUG variable can be either debug or debug-brk. This will also start node-inspector on port 8080 in the same container. 25 | 26 | -------------------------------------------------------------------------------- /lib/pre-publish.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"), 4 | path = require("path"), 5 | glob = require("glob"), 6 | opts = { 7 | cwd: path.join(__dirname, "..", "brackets-srv") 8 | }, 9 | logFile = "./install.log", 10 | exists = fs.existsSync(logFile), 11 | conts; 12 | 13 | if (exists) { 14 | conts = fs.readFileSync(logFile, { encoding: "utf8" }); 15 | fs.unlinkSync(logFile); 16 | } 17 | 18 | if (!conts) { 19 | glob("**/node_modules", opts, function (err, files) { 20 | if (err) { 21 | throw err; 22 | } 23 | 24 | if (files) { 25 | files.sort(function (a, b) { 26 | return b.length - a.length; 27 | }); 28 | 29 | files.forEach(function (file) { 30 | file = path.join(opts.cwd, file); 31 | fs.renameSync(file, file + "_"); 32 | console.log("file: " + file + "_"); 33 | }); 34 | } 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /docker/dev/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | MAINTAINER Boyan Rabchev 3 | 4 | ENV DEBIAN_FRONTEND noninteractive 5 | 6 | RUN apt-get update && \ 7 | apt-get -y install software-properties-common 8 | RUN add-apt-repository ppa:git-core/ppa && \ 9 | apt-get update && \ 10 | apt-get install -y git curl build-essential 11 | 12 | RUN curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash - && \ 13 | apt-get install -y nodejs 14 | 15 | RUN npm install -g npm && \ 16 | npm install -g grunt-cli && \ 17 | npm install -g node-inspector 18 | 19 | WORKDIR ~ 20 | RUN mkdir Projects && mkdir .brackets-srv 21 | 22 | WORKDIR /var 23 | RUN git clone https://github.com/rabchev/brackets-server.git 24 | 25 | WORKDIR /var/brackets-server 26 | RUN git submodule update --init --recursive && \ 27 | npm install && \ 28 | grunt build 29 | 30 | EXPOSE 6800 8080 31 | VOLUME ["~/Projects", "~/.brackets-srv", "/var/brackets-server"] 32 | 33 | COPY start.sh / 34 | 35 | ENTRYPOINT ["/start.sh"] 36 | CMD ["-d"] 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2016 Boyan Rabchev 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/domains/socket.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var ConnectionManager = require("./ConnectionManager"), 4 | DomainManager = require("./DomainManager"); 5 | 6 | function init(srv) { 7 | var root = srv.httpRoot + "-ext", 8 | apiUrl = root + "/api"; 9 | 10 | srv.httpServer.on("request", function (req, res) { 11 | if (req.url.startsWith(apiUrl)) { 12 | res.setHeader("Content-Type", "application/json"); 13 | res.end( 14 | JSON.stringify(DomainManager.getDomainDescriptions(), 15 | null, 16 | 4) 17 | ); 18 | } 19 | }); 20 | 21 | srv.io 22 | .of(root) 23 | .on("connection", ConnectionManager.createConnection); 24 | 25 | DomainManager.httpRoot = srv.httpRoot; 26 | DomainManager.supportDir = srv.supportDir; 27 | DomainManager.projectsDir = srv.projectsDir; 28 | DomainManager.samplesDir = srv.samplesDir; 29 | DomainManager.allowUserDomains = srv.allowUserDomains; 30 | DomainManager.loadDomainModulesFromPaths(["./BaseDomain"]); 31 | } 32 | 33 | exports.init = init; 34 | -------------------------------------------------------------------------------- /lib/shim.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | if (!String.prototype.startsWith) { 4 | var toString = {}.toString; 5 | String.prototype.startsWith = function (search){ 6 | if (this === null) { 7 | throw new TypeError(); 8 | } 9 | var string = String(this); 10 | if (search && toString.call(search) === "[object RegExp]") { 11 | throw new TypeError(); 12 | } 13 | var stringLength = string.length; 14 | var searchString = String(search); 15 | var searchLength = searchString.length; 16 | var position = arguments.length > 1 ? arguments[1] : undefined; 17 | 18 | var pos = position ? Number(position) : 0; 19 | if (pos !== pos) { 20 | pos = 0; 21 | } 22 | var start = Math.min(Math.max(pos, 0), stringLength); 23 | 24 | if (searchLength + start > stringLength) { 25 | return false; 26 | } 27 | var index = -1; 28 | while (++index < searchLength) { 29 | if (string.charCodeAt(start + index) !== searchString.charCodeAt(index)) { 30 | return false; 31 | } 32 | } 33 | return true; 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /hacks/low-level-fs.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports) { 2 | "use strict"; 3 | 4 | var fs = require("fileSystemImpl"), 5 | FileSystemError = require("filesystem/FileSystemError"); 6 | 7 | function StatMap(stats) { 8 | this._stats = stats; 9 | } 10 | 11 | StatMap.prototype.isDirectory = function () { 12 | return this._stats.isDirectory; 13 | }; 14 | 15 | StatMap.prototype.isFile = function () { 16 | return this._stats.isFile; 17 | }; 18 | 19 | function stat(path, callback) { 20 | fs.stat(path, function (err, stats) { 21 | if (err) { 22 | return callback(err); 23 | } 24 | callback(null, new StatMap(stats)); 25 | }); 26 | } 27 | 28 | function makedir(path, mode, callback) { 29 | fs.mkdir(path, parseInt(mode + "", 8), callback); 30 | } 31 | 32 | function readdir(path, callback) { 33 | fs.readdir(path, callback); 34 | } 35 | 36 | function copyFile(src, dest, callback) { 37 | fs.copyFile(src, dest, callback); 38 | } 39 | 40 | exports.stat = stat; 41 | exports.makedir = makedir; 42 | exports.readdir = readdir; 43 | exports.copyFile = copyFile; 44 | 45 | exports.NO_ERROR = null; 46 | exports.ERR_NOT_FOUND = FileSystemError.NOT_FOUND; 47 | }); 48 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.5.8 - 2014-05-05 2 | * Brackets version: 1.3 3 | * Fixed, issues with Brackets v1.3. (LiveDevelopment, HealthData) 4 | * Fixed, version number of socket.io: issue #6 5 | 6 | ## 0.5.7 - 2014-11-04 7 | * Brackets version: 0.44.0-0 8 | * Fixed, CORS problem with extensions. 9 | * Fixed, Node Domains path resolution for extensions. 10 | * Improvements in Open and Save dialogs. 11 | 12 | ## 0.5.6 - 2014-10-10 13 | * Brackets version: 0.44.0-0 14 | * Fixed, the problem described in v0.5.2 was not actually fixed. 15 | 16 | ## 0.5.5 - 2014-10-10 17 | * Brackets version: 0.44.0-0 18 | * Fixed, error if projects directory doesn't exist. 19 | 20 | ## 0.5.4 - 2014-10-09 21 | * Brackets version: 0.44.0-0 22 | * Updated Brackets source to sprint 44 23 | * Updated README.md file 24 | * Updated CONTRIBUTING.md file 25 | 26 | ## 0.5.3 - 2014-10-06 27 | * Brackets version: 0.44.0-beta 28 | * Automatically creates new projects root directory if it doesn’t exist. 29 | 30 | ## 0.5.2 - 2014-10-03 31 | * Brackets version: 0.44.0-beta 32 | * Fixed problem with NPM package causing malfunctioning of Open, Open Folder, Save As, Create new project and extension installations. 33 | 34 | ## 0.5.1 - 2014-10-02 35 | * Brackets version: 0.44.0-beta 36 | * Removed unneeded files from NPM package. 37 | 38 | ## 0.5.0 - 2014-10-01 39 | * Brackets version: 0.44.0-beta 40 | * Initial release. 41 | -------------------------------------------------------------------------------- /bin/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var commander = require("commander"), 4 | brackets = require("../"), 5 | pkg = require("../package.json"), 6 | open = require("open"), 7 | path = require("path"), 8 | homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE || process.cwd(); 9 | 10 | commander 11 | .version(pkg.version) 12 | .option("-p, --port ", "Specifies TCP for Brackets service. The default port is 6800.") 13 | .option("-o, --open", "Opens Brackets in the default web browser.") 14 | .option("-s, --supp-dir ", "Specifies the root directory for Brackets supporting files such as user extensions, configurations and state persistence. The default locations is ~/.brackets-srv.") 15 | .option("-j, --proj-dir ", "Specifies the root directory for projects. The default locations is ~/Projects.") 16 | .option("-d, --user-domains", "Allows Node domains to be loaded from user extensions.") 17 | .parse(process.argv); 18 | 19 | var app = brackets(commander.port, { 20 | supportDir: commander.suppDir || path.join(homeDir, ".brackets-srv"), 21 | projectsDir: commander.projDir || path.join(homeDir, "Projects"), 22 | allowUserDomains: commander.userDomains 23 | }); 24 | 25 | if (commander.open) { 26 | open("http://localhost:" + app.httpServer.address().port); 27 | } 28 | -------------------------------------------------------------------------------- /test/server_test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var request = require("supertest"), 4 | chai = require("chai"), 5 | server = require("./app"), 6 | expect = chai.expect, 7 | agent; 8 | 9 | function test(done, setReq, examine) { 10 | var req = setReq(agent); 11 | req.end(function (err, res) { 12 | if (err) { 13 | return done(err); 14 | } 15 | if (examine) { 16 | var data; 17 | if (res.body && Object.keys(res.body).length) { 18 | data = res.body; 19 | } else { 20 | data = res.text; 21 | } 22 | examine(data); 23 | done(); 24 | } else { 25 | done(); 26 | } 27 | }); 28 | } 29 | 30 | describe("swaggy", function () { 31 | 32 | before(function (done) { 33 | server.init(function (err, app) { 34 | if (err) { 35 | return done(err); 36 | } 37 | 38 | agent = request.agent(app); 39 | done(); 40 | }); 41 | }); 42 | 43 | it("get index", function (done) { 44 | test(done, function (req) { 45 | return req 46 | .get("/") 47 | .set("Accept", "text/html") 48 | .expect("Content-Type", /html/) 49 | .expect(200); 50 | }, function (data) { 51 | expect(data).to.equal("Hello"); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brackets", 3 | "description": "Brackets Server is a server for providing hosted version of the popular code editor Brackets.", 4 | "version": "0.5.8", 5 | "author": "Boyan Rabchev ", 6 | "contributors": [ 7 | { 8 | "name": "Boyan Rabchev", 9 | "email": "boyan@rabchev.com" 10 | } 11 | ], 12 | "repository": "https://github.com/rabchev/brackets-server.git", 13 | "main": "./lib/server", 14 | "preferGlobal": "true", 15 | "bin": { 16 | "brackets": "./bin/run.js", 17 | "brackets-server": "./bin/run.js" 18 | }, 19 | "dependencies": { 20 | "commander": "^2.3.0", 21 | "glob": "^4.0.6", 22 | "mkdirp": "^0.5.0", 23 | "ncp": "^1.0.0", 24 | "open": "^0.0.5", 25 | "rimraf": "^2.2.8", 26 | "send": "^0.9.3", 27 | "socket.io": "1.1.0", 28 | "socket.io-client": "^1.4.0" 29 | }, 30 | "devDependencies": { 31 | "mocha": "*", 32 | "chai": "*", 33 | "supertest": "*", 34 | "grunt": "*", 35 | "grunt-release": "*", 36 | "grunt-shell": "*", 37 | "grunt-simple-mocha": "*", 38 | "grunt-node-inspector": "*", 39 | "grunt-concurrent": "*", 40 | "grunt-contrib-requirejs": "*", 41 | "grunt-contrib-compress": "*", 42 | "grunt-contrib-copy": "*", 43 | "grunt-contrib-htmlmin": "*", 44 | "grunt-contrib-less": "0.8.2", 45 | "grunt-replace": "*", 46 | "grunt-contrib-concat": "*", 47 | "grunt-targethtml": "*", 48 | "grunt-usemin": "0.1.11", 49 | "grunt-contrib-clean": "*", 50 | "load-grunt-tasks": "*", 51 | "grunt-text-replace": "*", 52 | "q": "0.9.2", 53 | "shelljs": "^0.3.0" 54 | }, 55 | "keywords": [ 56 | "brackets", 57 | "node", 58 | "nodejs", 59 | "code", 60 | "editor", 61 | "web", 62 | "project", 63 | "application", 64 | "management", 65 | "dev", 66 | "development", 67 | "javascript", 68 | "html", 69 | "css", 70 | "ide" 71 | ], 72 | "scripts": { 73 | "test": "grunt test", 74 | "postinstall": "node lib/post-install.js" 75 | }, 76 | "bugs": { 77 | "email": "boyan@rabchev.com", 78 | "url": "https://github.com/rabchev/brackets-server/issues" 79 | }, 80 | "license": "MIT", 81 | "engines": { 82 | "node": ">=0.10" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /hacks/app.js: -------------------------------------------------------------------------------- 1 | // Shell app mock 2 | 3 | define(function (require, exports) { 4 | "use strict"; 5 | 6 | exports.getApplicationSupportDirectory = function () { 7 | return "/support"; 8 | }; 9 | 10 | 11 | exports.getUserDocumentsDirectory = function () { 12 | return "/projects"; 13 | }; 14 | 15 | /** 16 | * Returns the TCP port of the current Node server 17 | * 18 | * @param {function(err, port)} callback Asynchronous callback function. The callback gets two arguments 19 | * (err, port) where port is the TCP port of the running server. 20 | * Possible error values: 21 | * ERR_NODE_PORT_NOT_SET = -1; 22 | * ERR_NODE_NOT_RUNNING = -2; 23 | * ERR_NODE_FAILED = -3; 24 | * 25 | * @return None. This is an asynchronous call that sends all return information to the callback. 26 | */ 27 | exports.getNodeState = function (callback) { 28 | // We serve the source from Node, connect to the same instance. 29 | callback(null, window.location.port || window.location.protocol === "http" ? 80 : 443); 30 | }; 31 | 32 | exports.openURLInDefaultBrowser = function (url) { 33 | var win = window.open(url, "_blank"); 34 | win.focus(); 35 | }; 36 | 37 | exports.quit = function () { 38 | // Browser window cannot be closed from script. 39 | }; 40 | 41 | exports.abortQuit = function () { 42 | // Browser window cannot be closed from script. 43 | }; 44 | 45 | exports.showExtensionsFolder = function (appURL, callback) { 46 | // TODO: See if this is needed at all, if so we have to provide some sort of file explorer. 47 | callback("Not supported."); 48 | }; 49 | 50 | var Fn = Function, global = (new Fn("return this"))(); 51 | if (!global.Mustache.compile) { 52 | global.Mustache.compile = function (template) { 53 | // This operation parses the template and caches 54 | // the resulting token tree. All future calls to 55 | // mustache.render can now skip the parsing step. 56 | global.Mustache.parse(template); 57 | 58 | return function (view, partials) { 59 | return global.Mustache.render(template, view, partials); 60 | }; 61 | }; 62 | } 63 | 64 | $.ajaxPrefilter(function(options) { 65 | if (options.crossDomain) { 66 | options.url = window.location.pathname + "proxy/" + encodeURIComponent(options.url); 67 | options.crossDomain = false; 68 | } 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Steps to build Brackets Server 5 | ------------------------------ 6 | 7 | Grunt CLI is a prerequisite. 8 | 9 | ```shell 10 | $ git clone https://github.com/rabchev/brackets-server.git 11 | $ cd brackets-server 12 | $ git submodule update --recursive --init 13 | $ npm install 14 | $ grunt build 15 | ``` 16 | 17 | To start Brackets Server: 18 | 19 | ```shell 20 | $ node bin/run -d 21 | ``` 22 | 23 | To run all tests: 24 | 25 | ```shell 26 | $ grunt test 27 | ``` 28 | 29 | To debug client scripts, open Gruntfile.js and uncomment all occurrences of: 30 | 31 | ```javascript 32 | // generateSourceMaps: true, 33 | // useSourceUrl: true, 34 | ``` 35 | 36 | Steps to update Adobe Brackets source code 37 | ------------------------------------------ 38 | 39 | NOTE: Usually, updating Brackets source requires fixing conflicts and compatibility issues with Brackets Server. 40 | 41 | ```shell 42 | 43 | $ git clone https://github.com/rabchev/brackets-server.git 44 | $ git submodule update --init 45 | $ cd brackets-src 46 | $ git fetch --tags 47 | $ git checkout tags/[release_tag_name] 48 | $ cd .. 49 | $ git commit -am "Updated Brackets source to verion ..." 50 | $ git submodule update --init --recursive 51 | $ npm install 52 | $ grunt build 53 | 54 | ``` 55 | 56 | Directory Structure 57 | ------------------- 58 | 59 | - **brackets-src** - This is Git submodule to https://github.com/adobe/brackets.git 60 | - **embedded-ext** - Contains embedded Brackets extensions. All extensions located in this folder are optimized and copied to `/brackets-dist/extensions/default` folder at build time. 61 | - **brackets-dist** - This is the output folder of the build process. All client script and CSS files are minified, combined and then copied to this folder. Some scripts are modified or replaced with hacked versions during optimization phase. 62 | - **brackets-srv** - Contains default Node.js domains, e.g. `StaticServerDomain` and `ExtensionMangerDomain`. These are separated from brackets-dist folder as brackets-dist is meant to contain only client side scripts. 63 | - **haks** - Contains scripts that replace their original counterparts entirely. For more details, please see the comments in the files. These files may require extra care when upgrading newer Brackets source. 64 | 65 | **NOTE:** `brackets-dist` and `brackets-src` are deleted and recreated entirely on every build. That’s why they are excluded from Git. The following folders are not necessary at run time and therefore they are excluded from the NPM package: `brackets-src`, `embedded-ext`, `hacks`, `examples`, `test`. 66 | 67 | Hacks List 68 | ---------- 69 | 70 | 1. **Shell app** - TODO: needs explanation 71 | 2. **Low level file system** i.e. the global object `brackets.fs` - TODO: needs explanation 72 | 3. **NodeConnection** - TODO: needs explanation 73 | 4. **File system implementation** i.e. `remote-fs` extension - TODO: needs explanation 74 | 5. **Menu Changes** - TODO: needs explanation 75 | 6. **Browser warning** - TODO: needs explanation 76 | 7. **CORS problems** - TODO: needs explanation 77 | -------------------------------------------------------------------------------- /lib/file-sys/native.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"), 4 | ncp = require("ncp"), 5 | mkdirp = require("mkdirp"), 6 | rimraf = require("rimraf"), 7 | pathUtil = require("path"), 8 | suppRoot = "/support/", 9 | sampRoot = "/samples/", 10 | projRoot = "/projects/"; 11 | 12 | function resolvePath(reqPath, context, callback) { 13 | var extRoot = context.httpRoot + "/extensions/", 14 | userExt = context.httpRoot + "/extensions/user", 15 | root, 16 | err, 17 | res; 18 | 19 | if (reqPath.startsWith(projRoot)) { 20 | root = context.projectsDir; 21 | res = pathUtil.join(root, reqPath.substr(projRoot.length)); 22 | } else if (reqPath.startsWith(userExt)) { 23 | root = context.supportDir + "/extensions/user"; 24 | res = pathUtil.join(root, reqPath.substr(userExt.length)); 25 | } else if (reqPath.startsWith(extRoot)) { 26 | root = context.defaultExtensions; 27 | res = pathUtil.join(context.defaultExtensions, reqPath.substr(extRoot.length)); 28 | } else if (reqPath.startsWith(suppRoot)) { 29 | root = context.supportDir; 30 | res = pathUtil.join(context.supportDir, reqPath.substr(suppRoot.length)); 31 | } else if (reqPath.startsWith(sampRoot)) { 32 | root = context.samplesDir; 33 | res = pathUtil.join(context.samplesDir, reqPath.substr(sampRoot.length)); 34 | } else { 35 | err = new Error("No such file or directory."); 36 | err.code = "ENOENT"; 37 | return callback(err); 38 | } 39 | 40 | if (res.substr(0, root.length) !== root) { 41 | err = new Error("Permission denied."); 42 | err.code = "EACCES"; 43 | callback(err); 44 | } else { 45 | callback(null, res); 46 | } 47 | } 48 | 49 | function stat(path, callback) { 50 | fs.stat(path, function (err, stats) { 51 | if (err) { 52 | return callback(err); 53 | } 54 | 55 | callback(null, { 56 | isFile: stats.isFile(), 57 | mtime: stats.mtime, 58 | size: stats.size, 59 | realPath: null, // TODO: Set real path if symbolic link. 60 | hash: stats.mtime.getTime() 61 | }); 62 | }); 63 | } 64 | 65 | function readdir(path, callback) { 66 | fs.readdir(path, callback); 67 | } 68 | 69 | function mkdir(path, mode, callback) { 70 | mkdirp(path, { mode: mode }, callback); 71 | } 72 | 73 | function rename(oldPath, newPath, callback) { 74 | fs.rename(oldPath, newPath, callback); 75 | } 76 | 77 | function readFile(path, encoding, callback) { 78 | fs.readFile(path, { encoding: encoding }, callback); 79 | } 80 | 81 | function writeFile(path, data, encoding, callback) { 82 | fs.writeFile(path, data, { encoding: encoding }, callback); 83 | } 84 | 85 | function unlink(path, callback) { 86 | rimraf(path, callback); 87 | } 88 | 89 | function moveToTrash(path, callback) { 90 | rimraf(path, callback); 91 | } 92 | 93 | function watchPath(req, callback) { 94 | callback(); 95 | } 96 | 97 | function unwatchPath(req, callback) { 98 | callback(); 99 | } 100 | 101 | function unwatchAll(req, callback) { 102 | callback(); 103 | } 104 | 105 | function copyFile(src, dest, callback) { 106 | ncp(src, dest, callback); 107 | } 108 | 109 | exports.resolvePath = resolvePath; 110 | exports.stat = stat; 111 | exports.readdir = readdir; 112 | exports.mkdir = mkdir; 113 | exports.rename = rename; 114 | exports.readFile = readFile; 115 | exports.writeFile = writeFile; 116 | exports.unlink = unlink; 117 | exports.moveToTrash = moveToTrash; 118 | exports.watchPath = watchPath; 119 | exports.unwatchPath = unwatchPath; 120 | exports.unwatchAll = unwatchAll; 121 | exports.copyFile = copyFile; 122 | -------------------------------------------------------------------------------- /lib/files.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function init(srv) { 4 | var fs = srv.fileSystem; 5 | 6 | function stat(req, callback) { 7 | fs.resolvePath(req, srv, function (err, path) { 8 | if (err) { 9 | return callback({ err: err }); 10 | } 11 | 12 | fs.stat(path, function (err, stats) { 13 | callback(err ? { err: err } : { stats: stats }); 14 | }); 15 | }); 16 | } 17 | 18 | function readdir(req, callback) { 19 | fs.resolvePath(req, srv, function (err, path) { 20 | if (err) { 21 | return callback({ err: err }); 22 | } 23 | 24 | fs.readdir(path, function (err, files) { 25 | callback({ err: err, contents: files }); 26 | }); 27 | }); 28 | } 29 | 30 | function mkdir(req, callback) { 31 | fs.resolvePath(req.path, srv, function (err, path) { 32 | if (err) { 33 | return callback(err); 34 | } 35 | 36 | fs.mkdir(path, req.mode, callback); 37 | }); 38 | } 39 | 40 | function rename(req, callback) { 41 | fs.resolvePath(req.oldPath, srv, function (err, oldPath) { 42 | if (err) { 43 | return callback(err); 44 | } 45 | fs.resolvePath(req.newPath, srv, function (err, newPath) { 46 | if (err) { 47 | return callback(err); 48 | } 49 | 50 | fs.rename(oldPath, newPath, callback); 51 | }); 52 | }); 53 | } 54 | 55 | function readFile(req, callback) { 56 | fs.resolvePath(req.path, srv, function (err, path) { 57 | if (err) { 58 | return callback({ err: err }); 59 | } 60 | 61 | fs.readFile(path, req.encoding, function (err, data) { 62 | callback({ err: err, data: data }); 63 | }); 64 | }); 65 | } 66 | 67 | function writeFile(req, callback) { 68 | fs.resolvePath(req.path, srv, function (err, path) { 69 | if (err) { 70 | return callback(err); 71 | } 72 | 73 | fs.writeFile(path, req.data, req.encoding, callback); 74 | }); 75 | } 76 | 77 | function unlink(req, callback) { 78 | fs.resolvePath(req, srv, function (err, path) { 79 | if (err) { 80 | return callback(err); 81 | } 82 | 83 | fs.unlink(path, callback); 84 | }); 85 | } 86 | 87 | function moveToTrash(req, callback) { 88 | fs.resolvePath(req, srv, function (err, path) { 89 | if (err) { 90 | return callback(err); 91 | } 92 | 93 | fs.moveToTrash(path, callback); 94 | }); 95 | } 96 | 97 | function watchPath(req, callback) { 98 | fs.resolvePath(req, srv, function (err, path) { 99 | if (err) { 100 | return callback(err); 101 | } 102 | 103 | fs.watchPath(path, callback); 104 | }); 105 | } 106 | 107 | function unwatchPath(req, callback) { 108 | fs.resolvePath(req, srv, function (err, path) { 109 | if (err) { 110 | return callback(err); 111 | } 112 | 113 | fs.unwatchPath(path, callback); 114 | }); 115 | } 116 | 117 | function unwatchAll(req, callback) { 118 | fs.resolvePath(req, srv, function (err, path) { 119 | if (err) { 120 | return callback(err); 121 | } 122 | 123 | fs.unwatchAll(path, callback); 124 | }); 125 | } 126 | 127 | function copyFile(req, callback) { 128 | fs.resolvePath(req.src, srv, function (err, src) { 129 | if (err) { 130 | return callback(err); 131 | } 132 | fs.resolvePath(req.dest, srv, function (err, dest) { 133 | if (err) { 134 | return callback(err); 135 | } 136 | 137 | fs.rename(src, dest, callback); 138 | }); 139 | }); 140 | } 141 | 142 | function onConnection (socket) { 143 | socket.emit("greeting", "hi"); 144 | 145 | socket 146 | .on("stat", stat) 147 | .on("mkdir", mkdir) 148 | .on("readdir", readdir) 149 | .on("rename", rename) 150 | .on("readFile", readFile) 151 | .on("writeFile", writeFile) 152 | .on("unlink", unlink) 153 | .on("moveToTrash", moveToTrash) 154 | .on("watchPath", watchPath) 155 | .on("unwatchPath", unwatchPath) 156 | .on("unwatchAll", unwatchAll) 157 | .on("copyFile", copyFile); 158 | } 159 | 160 | srv.io 161 | .of(srv.httpRoot) 162 | .on("connection", onConnection); 163 | } 164 | 165 | exports.init = init; 166 | -------------------------------------------------------------------------------- /lib/domains/BaseDomain.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | (function () { 25 | "use strict"; 26 | 27 | var //Launcher = require("./Launcher"), 28 | Logger = require("./Logger"); 29 | 30 | /** 31 | * @private 32 | * @type {DomainManager} 33 | * DomainManager provided at initialization time 34 | */ 35 | var _domainManager = null; 36 | 37 | /** 38 | * @private 39 | * Implementation of base.enableDebugger commnad. 40 | * In the future, process._debugProcess may go away. In that case 41 | * we will probably have to implement re-launching of the Node process 42 | * with the --debug command line switch. 43 | */ 44 | function cmdEnableDebugger() { 45 | // Unfortunately, there's no indication of whether this succeeded 46 | // This is the case for _all_ of the methods for enabling the debugger. 47 | process._debugProcess(process.pid); 48 | } 49 | 50 | /** 51 | * @private 52 | * Implementation of base.restartNode command. 53 | */ 54 | function cmdRestartNode() { 55 | // Launcher.exit(); 56 | } 57 | 58 | /** 59 | * @private 60 | * Implementation of base.loadDomainModulesFromPaths 61 | * @param {Array.} paths Paths to load 62 | * @return {boolean} Whether the load succeeded 63 | */ 64 | function cmdLoadDomainModulesFromPaths(paths) { 65 | if (_domainManager) { 66 | var success = _domainManager.loadDomainModulesFromPaths(paths); 67 | if (success) { 68 | _domainManager.emitEvent("base", "newDomains"); 69 | } 70 | return success; 71 | } else { 72 | return false; 73 | } 74 | } 75 | 76 | /** 77 | * 78 | * Registers commands with the DomainManager 79 | * @param {DomainManager} domainManager The DomainManager to use 80 | */ 81 | function init(domainManager) { 82 | _domainManager = domainManager; 83 | 84 | _domainManager.registerDomain("base", {major: 0, minor: 1}); 85 | _domainManager.registerCommand( 86 | "base", 87 | "enableDebugger", 88 | cmdEnableDebugger, 89 | false, 90 | "Attempt to enable the debugger", 91 | [], // no parameters 92 | [] // no return type 93 | ); 94 | _domainManager.registerCommand( 95 | "base", 96 | "restartNode", 97 | cmdRestartNode, 98 | false, 99 | "Attempt to restart the Node server", 100 | [], // no parameters 101 | [] // no return type 102 | ); 103 | _domainManager.registerCommand( 104 | "base", 105 | "loadDomainModulesFromPaths", 106 | cmdLoadDomainModulesFromPaths, 107 | false, 108 | "Attempt to load command modules from the given paths. " + 109 | "The paths should be absolute.", 110 | [{name: "paths", type: "array"}], 111 | [{name: "success", type: "boolean"}] 112 | ); 113 | 114 | _domainManager.registerEvent( 115 | "base", 116 | "log", 117 | [{name: "level", type: "string"}, 118 | {name: "timestamp", type: "Date"}, 119 | {name: "message", type: "string"}] 120 | ); 121 | Logger.on( 122 | "log", 123 | function (level, timestamp, message) { 124 | _domainManager.emitEvent( 125 | "base", 126 | "log", 127 | [level, timestamp, message] 128 | ); 129 | } 130 | ); 131 | 132 | _domainManager.registerEvent("base", "newDomains", []); 133 | } 134 | 135 | exports.init = init; 136 | 137 | }()); 138 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : true, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 14 | "indent" : 4, // {int} Number of spaces to use for indentation 15 | "latedef" : true, // true: Require variables/functions to be defined before being used 16 | "newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()` 17 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 18 | "noempty" : true, // true: Prohibit use of empty blocks 19 | "nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment) 20 | "plusplus" : false, // true: Prohibit use of `++` & `--` 21 | "quotmark" : "double", // Quotation mark consistency: 22 | // false : do nothing (default) 23 | // true : ensure whatever is used is consistent 24 | // "single" : require single quotes 25 | // "double" : require double quotes 26 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 27 | "unused" : true, // true: Require all defined variables be used 28 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 29 | "trailing" : true, // true: Prohibit trailing whitespaces 30 | "maxparams" : false, // {int} Max number of formal params allowed per function 31 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 32 | "maxstatements" : false, // {int} Max number statements per function 33 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 34 | "maxlen" : false, // {int} Max number of characters per line 35 | 36 | // Relaxing 37 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 38 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 39 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 40 | "eqnull" : false, // true: Tolerate use of `== null` 41 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 42 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) 43 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 44 | // (ex: `for each`, multiple try/catch, function expression…) 45 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 46 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 47 | "funcscope" : false, // true: Tolerate defining variables inside control statements" 48 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 49 | "iterator" : false, // true: Tolerate using the `__iterator__` property 50 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 51 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 52 | "laxcomma" : false, // true: Tolerate comma-first style coding 53 | "loopfunc" : false, // true: Tolerate functions being defined in loops 54 | "multistr" : false, // true: Tolerate multi-line strings 55 | "proto" : false, // true: Tolerate using the `__proto__` property 56 | "scripturl" : false, // true: Tolerate script-targeted URLs 57 | "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment 58 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 59 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 60 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 61 | "validthis" : false, // true: Tolerate using this in a non-constructor function 62 | 63 | // Environments 64 | "browser" : false, // Web Browser (window, document, etc) 65 | "couch" : false, // CouchDB 66 | "devel" : true, // Development/debugging (alert, confirm, etc) 67 | "dojo" : false, // Dojo Toolkit 68 | "jquery" : false, // jQuery 69 | "mootools" : false, // MooTools 70 | "node" : true, // Node.js 71 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 72 | "prototypejs" : false, // Prototype and Scriptaculous 73 | "rhino" : false, // Rhino 74 | "worker" : false, // Web Workers 75 | "wsh" : false, // Windows Scripting Host 76 | "yui" : false, // Yahoo User Interface 77 | 78 | // Legacy 79 | "nomen" : false, // true: Prohibit dangling `_` in variables 80 | "onevar" : false, // true: Allow only one `var` statement per function 81 | "passfail" : false, // true: Stop on first error 82 | "white" : false, // true: Check against strict whitespace and indentation rules 83 | 84 | // Custom Globals 85 | "globals" : {} // additional predefined global variables 86 | } 87 | -------------------------------------------------------------------------------- /hacks/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : true, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 14 | "indent" : 4, // {int} Number of spaces to use for indentation 15 | "latedef" : true, // true: Require variables/functions to be defined before being used 16 | "newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()` 17 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 18 | "noempty" : true, // true: Prohibit use of empty blocks 19 | "nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment) 20 | "plusplus" : false, // true: Prohibit use of `++` & `--` 21 | "quotmark" : "double", // Quotation mark consistency: 22 | // false : do nothing (default) 23 | // true : ensure whatever is used is consistent 24 | // "single" : require single quotes 25 | // "double" : require double quotes 26 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 27 | "unused" : true, // true: Require all defined variables be used 28 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 29 | "trailing" : true, // true: Prohibit trailing whitespaces 30 | "maxparams" : false, // {int} Max number of formal params allowed per function 31 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 32 | "maxstatements" : false, // {int} Max number statements per function 33 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 34 | "maxlen" : false, // {int} Max number of characters per line 35 | 36 | // Relaxing 37 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 38 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 39 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 40 | "eqnull" : false, // true: Tolerate use of `== null` 41 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 42 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) 43 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 44 | // (ex: `for each`, multiple try/catch, function expression…) 45 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 46 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 47 | "funcscope" : false, // true: Tolerate defining variables inside control statements" 48 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 49 | "iterator" : false, // true: Tolerate using the `__iterator__` property 50 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 51 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 52 | "laxcomma" : false, // true: Tolerate comma-first style coding 53 | "loopfunc" : false, // true: Tolerate functions being defined in loops 54 | "multistr" : false, // true: Tolerate multi-line strings 55 | "proto" : false, // true: Tolerate using the `__proto__` property 56 | "scripturl" : false, // true: Tolerate script-targeted URLs 57 | "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment 58 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 59 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 60 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 61 | "validthis" : false, // true: Tolerate using this in a non-constructor function 62 | 63 | // Environments 64 | "browser" : true, // Web Browser (window, document, etc) 65 | "couch" : false, // CouchDB 66 | "devel" : true, // Development/debugging (alert, confirm, etc) 67 | "dojo" : false, // Dojo Toolkit 68 | "jquery" : true, // jQuery 69 | "mootools" : false, // MooTools 70 | "node" : false, // Node.js 71 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 72 | "prototypejs" : false, // Prototype and Scriptaculous 73 | "rhino" : false, // Rhino 74 | "worker" : false, // Web Workers 75 | "wsh" : false, // Windows Scripting Host 76 | "yui" : false, // Yahoo User Interface 77 | 78 | // Legacy 79 | "nomen" : false, // true: Prohibit dangling `_` in variables 80 | "onevar" : false, // true: Allow only one `var` statement per function 81 | "passfail" : false, // true: Stop on first error 82 | "white" : false, // true: Check against strict whitespace and indentation rules 83 | 84 | // Custom Globals 85 | "globals" : { 86 | "brackets" : true, 87 | "define" : true 88 | } // additional predefined global variables 89 | } 90 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : true, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 14 | "indent" : 4, // {int} Number of spaces to use for indentation 15 | "latedef" : true, // true: Require variables/functions to be defined before being used 16 | "newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()` 17 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 18 | "noempty" : true, // true: Prohibit use of empty blocks 19 | "nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment) 20 | "plusplus" : false, // true: Prohibit use of `++` & `--` 21 | "quotmark" : "double", // Quotation mark consistency: 22 | // false : do nothing (default) 23 | // true : ensure whatever is used is consistent 24 | // "single" : require single quotes 25 | // "double" : require double quotes 26 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 27 | "unused" : true, // true: Require all defined variables be used 28 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 29 | "trailing" : true, // true: Prohibit trailing whitespaces 30 | "maxparams" : false, // {int} Max number of formal params allowed per function 31 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 32 | "maxstatements" : false, // {int} Max number statements per function 33 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 34 | "maxlen" : false, // {int} Max number of characters per line 35 | 36 | // Relaxing 37 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 38 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 39 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 40 | "eqnull" : false, // true: Tolerate use of `== null` 41 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 42 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) 43 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 44 | // (ex: `for each`, multiple try/catch, function expression…) 45 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 46 | "expr" : true, // true: Tolerate `ExpressionStatement` as Programs 47 | "funcscope" : false, // true: Tolerate defining variables inside control statements" 48 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 49 | "iterator" : false, // true: Tolerate using the `__iterator__` property 50 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 51 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 52 | "laxcomma" : false, // true: Tolerate comma-first style coding 53 | "loopfunc" : false, // true: Tolerate functions being defined in loops 54 | "multistr" : false, // true: Tolerate multi-line strings 55 | "proto" : false, // true: Tolerate using the `__proto__` property 56 | "scripturl" : false, // true: Tolerate script-targeted URLs 57 | "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment 58 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 59 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 60 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 61 | "validthis" : false, // true: Tolerate using this in a non-constructor function 62 | 63 | // Environments 64 | "browser" : false, // Web Browser (window, document, etc) 65 | "couch" : false, // CouchDB 66 | "devel" : true, // Development/debugging (alert, confirm, etc) 67 | "dojo" : false, // Dojo Toolkit 68 | "jquery" : false, // jQuery 69 | "mootools" : false, // MooTools 70 | "node" : true, // Node.js 71 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 72 | "prototypejs" : false, // Prototype and Scriptaculous 73 | "rhino" : false, // Rhino 74 | "worker" : false, // Web Workers 75 | "wsh" : false, // Windows Scripting Host 76 | "yui" : false, // Yahoo User Interface 77 | 78 | // Legacy 79 | "nomen" : false, // true: Prohibit dangling `_` in variables 80 | "onevar" : false, // true: Allow only one `var` statement per function 81 | "passfail" : false, // true: Stop on first error 82 | "white" : false, // true: Check against strict whitespace and indentation rules 83 | 84 | // Custom Globals 85 | "globals" : { // additional predefined global variables 86 | "it": true, 87 | "describe": true, 88 | "before": true 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | IMPORTANT 2 | ========= 3 | There is a new project derived from this one: [nodeSpeed IDE](https://github.com/whoGloo/nodespeed-ide). 4 | 5 | Since I’m not able to actively maintain this project, I encourage everyone interested to use and support the new one. 6 | 7 | Brackets Server 8 | =============== 9 | 10 | Brackets Server is a server for providing hosted version of the popular code editor [Brackets](http://brackets.io/). The code editor can be loaded directly in the web browser and it doesn’t require additional installations or browser extensions. Brackets works just like the desktop version, except that all projects and files reside on the server instead of the local file system. 11 | 12 | The server may be useful for remote development, real-time changes and testing, development form thin clients or devices such as tablets, or it could be used in conjunction with other web applications for collaboration. 13 | 14 | To check the current verion of Brackets source used in the server, please see [CHANGELOG](https://github.com/rabchev/brackets-server/blob/master/CHANGELOG.md). 15 | 16 | Installation 17 | ------------ 18 | 19 | Install from npm: 20 | 21 | $ npm install brackets -g 22 | 23 | Usage Examples 24 | -------------- 25 | 26 | $ brackets --port 80 --proj-dir /var/projects --supp-dir /var/brackets 27 | 28 | **IMPORTANT:** Make sure ***projects*** directory exists. 29 | 30 | **IMPORTANT:** Brackets Server cannot work simultaneously on the same machine with the desktop Brackets because of 31 | port conflict in one of the build-in modules. The error thrown is: "Error: listen EADDRINUSE". 32 | To workaround this problem if you ever need to use bouth simultaneously, run Brackets Server in Docker container. 33 | 34 | All arguments are optional. 35 | 36 | | Short Option | Long Option | Default Value | Description 37 | |--------------|------------------|-------------------|------------------------------------------------------------ 38 | | `-p ` | `--port` | `6800` | TCP port on which Brackets server is listening. 39 | | `-j ` | `--proj-dir` | `~/Projects` | Root directory for projects. Directories above this root cannot be accessed. 40 | | `-s ` | `--supp-dir` | `~/.brackets-srv` | Root directory for Brackets supporting files such as user extensions, configurations and state persistence. 41 | | `-d` | `--user-domains` | `false` | Allows Node domains to be loaded from user extensions. 42 | 43 | **NOTE:** Some Brackets extensions require external Node.js process, called node domain. Node domains run on the server, thereby allowing arbitrary code to be executed on the server through custom extensions. Since this imposes very serious security and stability risks, Brackets Server will not load nor execute domains from user extensions, unless `-d` option is specified. 44 | 45 | Embedding Brackets Server in Web Applications 46 | --------------------------------------------- 47 | 48 | Example with Express: 49 | 50 | ```javascript 51 | var path = require("path"), 52 | http = require("http"), 53 | express = require("express"), 54 | brackets = require("brackets"), 55 | app = express(), 56 | server = http.createServer(app); 57 | 58 | app.get("/", function (req, res) { 59 | res.send("Hello World"); 60 | }); 61 | 62 | var bracketsOpts = { 63 | projectsDir: path.join(__dirname, ".."), 64 | supportDir: path.join(__dirname, "..", "/support") 65 | }; 66 | brackets(server, bracketsOpts); 67 | 68 | server.listen(3000); 69 | 70 | console.log("Your application is availble at http://localhost:3000"); 71 | console.log("You can access Brackets on http://localhost:3000/brackets/"); 72 | ``` 73 | 74 | **NOTE:** The default values for `projectsDir` and `supportDir` are different when Brackets Server is initiated from code. They are respectively `./projects` and `./brackets`, relative to the current working directory. 75 | 76 | Options: 77 | 78 | | Option | Default Value | Description 79 | |------------------|-------------------|------------------------------------------------------------ 80 | | httpRoot | `/brackets` | Defines the root HTTP endpoint for Brackets Server (http://yourdomain.com/brackets). 81 | | projectsDir | `./projects` | Root directory for projects. Directories above this root cannot be accessed. 82 | | supportDir | `./brackets` | Root directory for Brackets supporting files such as user extensions, configurations and state persistence. 83 | | allowUserDomains | `false` | Allows Node domains to be loaded from user extensions. 84 | 85 | 86 | Contributing 87 | ------------ 88 | 89 | Please see [`CONTRIBUTING.md`](https://github.com/rabchev/brackets-server/blob/master/CONTRIBUTING.md) 90 | 91 | License 92 | ------- 93 | 94 | (MIT License) 95 | 96 | Copyright (c) 2012 Boyan Rabchev . All rights reserved. 97 | 98 | Permission is hereby granted, free of charge, to any person obtaining 99 | a copy of this software and associated documentation files (the 100 | 'Software'), to deal in the Software without restriction, including 101 | without limitation the rights to use, copy, modify, merge, publish, 102 | distribute, sublicense, and/or sell copies of the Software, and to 103 | permit persons to whom the Software is furnished to do so, subject to 104 | the following conditions: 105 | 106 | The above copyright notice and this permission notice shall be 107 | included in all copies or substantial portions of the Software. 108 | 109 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 110 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 111 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 112 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 113 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 114 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 115 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 116 | -------------------------------------------------------------------------------- /lib/domains/Logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | (function () { 25 | "use strict"; 26 | 27 | var fs = require("fs"), 28 | util = require("util"), 29 | EventEmitter = require("events").EventEmitter; 30 | 31 | /** 32 | * @constructor 33 | * The Logger module is a singleton object used for logging. 34 | * Logger inherits from the EventEmitter class and exports itself 35 | * as the module. 36 | */ 37 | var Logger = module.exports = new EventEmitter(); 38 | 39 | /** 40 | * @private 41 | * @type{?string} 42 | * Filename to append all log data to. 43 | */ 44 | var _logFilename = null; 45 | 46 | /** 47 | * @private 48 | * @type{Array.<{level: string, timestamp: Date, message: string}>} 49 | * Complete log history 50 | */ 51 | var _logHistory = []; 52 | 53 | /** 54 | * @private 55 | * Helper function for logging functions. Handles string formatting. 56 | * @param {string} level Log level ("log", "info", etc.) 57 | * @param {Array.} Array of objects for logging. Works identically 58 | * to how objects can be passed to console.log. Uses util.format to 59 | * format into a single string. 60 | */ 61 | function logReplacement(level, args) { 62 | var message = util.format.apply(null, args); 63 | var timestamp = new Date(); 64 | if (_logFilename) { 65 | try { 66 | var timestampString = 67 | "[" + level + ": " + 68 | timestamp.toLocaleTimeString() + "] "; 69 | 70 | fs.appendFileSync(_logFilename, 71 | timestampString + message + "\n"); 72 | } catch (e) { } 73 | } 74 | _logHistory.push({ 75 | level: level, 76 | timestamp: timestamp, 77 | message: message 78 | }); 79 | Logger.emit("log", level, timestamp, message); 80 | } 81 | 82 | /** 83 | * Log a "log" message 84 | * @param {...Object} log arguments as in console.log etc. 85 | * First parameter can be a "format" string. 86 | */ 87 | function log() { logReplacement("log", arguments); } 88 | 89 | /** 90 | * Log an "info" message 91 | * @param {...Object} log arguments as in console.log etc. 92 | * First parameter can be a "format" string. 93 | */ 94 | function info() { logReplacement("info", arguments); } 95 | 96 | /** 97 | * Log a "warn" message 98 | * @param {...Object} log arguments as in console.log etc. 99 | * First parameter can be a "format" string. 100 | */ 101 | function warn() { logReplacement("warn", arguments); } 102 | 103 | /** 104 | * Log an "error" message 105 | * @param {...Object} log arguments as in console.log etc. 106 | * First parameter can be a "format" string. 107 | */ 108 | function error() { logReplacement("error", arguments); } 109 | 110 | /** 111 | * Log a "dir" message 112 | * @param {...Object} log arguments as in console.dir 113 | * Note that (just like console.dir) this does NOT do string 114 | * formatting using the first argument. 115 | */ 116 | function dir() { 117 | // dir does not do optional string formatting 118 | var args = Array.prototype.slice.call(arguments, 0); 119 | args.unshift("%s"); 120 | logReplacement("dir", args); 121 | } 122 | 123 | /** 124 | * Remaps the console.log, etc. functions to the logging functions 125 | * defined in this module. Useful so that modules can simply call 126 | * console.log to call into this Logger (since client doesn't have) 127 | * access to stdout. 128 | */ 129 | function remapConsole() { 130 | // Reassign logging functions to our logger 131 | // NOTE: console.timeEnd uses console.log and console.trace uses 132 | // console.error, so we don't need to change it explicitly 133 | console.log = log; 134 | console.info = info; 135 | console.warn = warn; 136 | console.error = error; 137 | console.dir = dir; 138 | } 139 | 140 | /** 141 | * Retrieves the entire log history 142 | * @return {Array.<{level: string, timestamp: Date, message: string}>} 143 | */ 144 | function getLogHistory(count) { 145 | if (count === null) { 146 | count = 0; 147 | } 148 | return _logHistory.slice(-count); 149 | } 150 | 151 | /** 152 | * Sets the filename to which the log messages are appended. 153 | * Specifying a null filename will turn off logging to a file. 154 | * @param {?string} filename The filename. 155 | */ 156 | function setLogFilename(filename) { 157 | _logFilename = filename; 158 | } 159 | 160 | // Public interface 161 | Logger.log = log; 162 | Logger.info = info; 163 | Logger.warn = warn; 164 | Logger.error = error; 165 | Logger.dir = dir; 166 | Logger.remapConsole = remapConsole; 167 | Logger.getLogHistory = getLogHistory; 168 | Logger.setLogFilename = setLogFilename; 169 | 170 | }()); 171 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | /*jshint -W086 */ 2 | 3 | "use strict"; 4 | 5 | // NOTE: Brackets doesn't fully support browsers yet and we need some workarounds. Workarounds will be marked with "HACK:" label. 6 | 7 | var http = require("http"), 8 | https = require("https"), 9 | path = require("path"), 10 | send = require("send"), 11 | util = require("util"), 12 | urlUtil = require("url"), 13 | files = require("./files"), 14 | domains = require("./domains/socket"), 15 | socket = require("socket.io"), 16 | brckDist = {root: path.join(__dirname, "..", "brackets-dist")}, 17 | zipped = { ".js": "application/javascript", ".css": "text/css"}, 18 | defaultPort = 6800; 19 | 20 | require("./shim"); 21 | 22 | function removeTrailingSlash(path) { 23 | return path[path.length - 1] === "/" ? path.substr(0, path.length - 1) : path; 24 | } 25 | 26 | function createHttpServer(inst, port) { 27 | inst.httpServer = http.createServer(function (req, res) { 28 | if (req.url === "/") { 29 | res.writeHead(302, {Location: inst.httpRoot + "/"}); 30 | res.end(); 31 | } else { 32 | res.writeHead(304); 33 | res.end("Not found"); 34 | } 35 | }); 36 | inst.io = socket(inst.httpServer); 37 | inst.httpServer.listen(port); 38 | console.log(util.format("\n listening on port %d\n", port)); 39 | } 40 | 41 | function attachStatic(inst) { 42 | var srv = inst.httpServer, 43 | root = inst.httpRoot, 44 | evs = srv.listeners("request").slice(0), 45 | extDir = { root: path.join(inst.supportDir, "extensions")} ; 46 | 47 | srv.removeAllListeners("request"); 48 | srv.on("request", function(req, res) { 49 | if (req.url.startsWith(root)) { 50 | var url = req.url.substr(root.length); 51 | 52 | if (url === "") { 53 | res.writeHead(301, {Location: inst.httpRoot + "/"}); 54 | res.end(); 55 | return; 56 | } 57 | 58 | if (url === "/") { 59 | url = "/index.html"; 60 | } 61 | 62 | if (url.startsWith("/proxy/")) { 63 | var reqUrl = decodeURIComponent(url.substr("/proxy/".length)), 64 | options = urlUtil.parse(reqUrl), 65 | httpClient = options.protocol === "http" ? http : https; 66 | 67 | delete options.protocol; 68 | options.method = "GET"; 69 | 70 | req.pause(); 71 | var connector = httpClient.request(options, function(_res) { 72 | _res.pause(); 73 | res.writeHead(_res.statusCode, _res.headers); 74 | _res.pipe(res); 75 | _res.resume(); 76 | }); 77 | req.pipe(connector); 78 | req.resume(); 79 | return; 80 | } 81 | 82 | var cntType = zipped[path.extname(url)]; 83 | if (cntType) { 84 | send(req, url + ".gz", brckDist) 85 | .on("headers", function (_res) { 86 | _res.setHeader("Content-Encoding", "gzip"); 87 | _res.setHeader("Content-Type", cntType); 88 | }) 89 | .pipe(res); 90 | return; 91 | } 92 | 93 | send(req, url, brckDist).pipe(res); 94 | } else if (req.url.startsWith("/support/extensions/")) { 95 | try { 96 | return send(req, req.url.substr("/support/extensions".length), extDir).pipe(res); 97 | } catch (e) { 98 | res.writeHead(500, { 99 | "Content-Length": e.message.length, 100 | "Content-Type": "text/plain" 101 | }); 102 | res.end(e.message); 103 | } 104 | } else { 105 | for (var i = 0; i < evs.length; i++) { 106 | evs[i].call(srv, req, res); 107 | } 108 | } 109 | }); 110 | } 111 | 112 | function Server(srv, opts) { 113 | if (!(this instanceof Server)) { 114 | return new Server(srv, opts); 115 | } 116 | 117 | switch (typeof srv) { 118 | case "undefined": 119 | case "null": 120 | createHttpServer(this, defaultPort); 121 | break; 122 | case "object": 123 | if (srv instanceof socket) { 124 | this.io = srv; 125 | this.httpServer = srv.httpServer; 126 | } else if (srv instanceof http.Server) { 127 | this.httpServer = srv; 128 | this.io = socket(this.httpServer); 129 | } else { 130 | opts = srv; 131 | srv = null; 132 | createHttpServer(this, defaultPort); 133 | } 134 | break; 135 | case "number": 136 | case "string": 137 | createHttpServer(this, Number(srv)); 138 | break; 139 | default: 140 | throw "Invalid argument – srv."; 141 | } 142 | 143 | opts = opts || {}; 144 | 145 | this.httpRoot = removeTrailingSlash(opts.httpRoot || "/brackets"); 146 | this.defaultExtensions = path.join(brckDist.root, "extensions"); 147 | this.supportDir = removeTrailingSlash(opts.supportDir || path.resolve("./brackets")); 148 | this.projectsDir = removeTrailingSlash(opts.projectsDir || path.resolve("./projects")); 149 | this.samplesDir = removeTrailingSlash(opts.samplesDir || path.join(brckDist.root, "samples")); 150 | this.allowUserDomains = opts.allowUserDomains || false; 151 | 152 | switch (typeof opts.fileSystem) { 153 | case "string": 154 | // Reserved for future build-in providers. 155 | this.fileSystem = require("./file-sys/" + opts.fileSystem); 156 | break; 157 | case "object": 158 | this.fileSystem = opts.fileSystem; 159 | break; 160 | case "undefined": 161 | case "null": 162 | this.fileSystem = require("./file-sys/native"); 163 | break; 164 | default: 165 | throw new Error("Invalid fileSystem option."); 166 | } 167 | 168 | var that = this; 169 | this.fileSystem.mkdir(this.projectsDir, function (err) { 170 | if (err && err.code !== "EEXIST") { 171 | throw err; 172 | } 173 | 174 | attachStatic(that); 175 | 176 | // Attach file system methods to socket.io. 177 | files.init(that); 178 | 179 | // Attach Brackets domians. 180 | domains.init(that); 181 | }); 182 | } 183 | 184 | module.exports = Server; 185 | -------------------------------------------------------------------------------- /lib/domains/ConnectionManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | (function () { 25 | "use strict"; 26 | 27 | var DomainManager = require("./DomainManager"); 28 | 29 | /** 30 | * @private 31 | * @type{Array.} 32 | * Currently active connections 33 | */ 34 | var _connections = []; 35 | 36 | /** 37 | * @private 38 | * @constructor 39 | * A WebSocket connection to a client. This is a private constructor. 40 | * Callers should use the ConnectionManager.createConnection function 41 | * instead. 42 | * @param {WebSocket} ws The WebSocket representing the client 43 | */ 44 | function Connection(ws) { 45 | this._ws = ws; 46 | this._connected = true; 47 | this._ws.on("message", this._receive.bind(this)); 48 | this._ws.on("close", this.close.bind(this)); 49 | } 50 | 51 | /** 52 | * @private 53 | * @type {boolean} 54 | * Whether the connection is connected. 55 | */ 56 | Connection.prototype._connected = false; 57 | 58 | /** 59 | * @private 60 | * @type {WebSocket} 61 | * The connection's WebSocket 62 | */ 63 | Connection.prototype._ws = null; 64 | 65 | /** 66 | * @private 67 | * Sends a message over the WebSocket. Called by public sendX commands. 68 | * @param {string} type Message type. Currently supported types are 69 | "event", "commandResponse", "commandError", "error" 70 | * @param {object} message Message body, must be JSON.stringify-able 71 | */ 72 | Connection.prototype._send = function (type, message) { 73 | if (this._ws && this._connected) { 74 | try { 75 | this._ws.send(JSON.stringify({type: type, message: message})); 76 | } catch (e) { 77 | console.error("[Connection] Unable to stringify message: " + e.message); 78 | } 79 | } 80 | }; 81 | 82 | /** 83 | * @private 84 | * Sends a binary message over the WebSocket. Implicitly interpreted as a 85 | * message of type "commandResponse". 86 | * @param {Buffer} message 87 | */ 88 | Connection.prototype._sendBinary = function (message) { 89 | if (this._ws && this._connected) { 90 | this._ws.send(message, {binary: true, mask: false}); 91 | } 92 | }; 93 | 94 | /** 95 | * @private 96 | * Receive event handler for the WebSocket. Responsible for parsing 97 | * message and handing it off to the appropriate handler. 98 | * @param {string} message Message received by WebSocket 99 | */ 100 | Connection.prototype._receive = function (message) { 101 | var m; 102 | try { 103 | m = JSON.parse(message); 104 | } catch (parseError) { 105 | this.sendError("Unable to parse message: " + message); 106 | return; 107 | } 108 | 109 | if (m.id !== null && m.id !== undefined && m.domain && m.command) { 110 | // okay if m.parameters is null/undefined 111 | try { 112 | DomainManager.executeCommand(this, m.id, m.domain, 113 | m.command, m.parameters); 114 | } catch (executionError) { 115 | this.sendCommandError(m.id, executionError.message, 116 | executionError.stack); 117 | } 118 | } else { 119 | this.sendError("Malformed message: " + message); 120 | } 121 | }; 122 | 123 | /** 124 | * Closes the connection and does necessary cleanup 125 | */ 126 | Connection.prototype.close = function () { 127 | if (this._ws) { 128 | try { 129 | this._ws.close(); 130 | } catch (e) { } 131 | } 132 | this._connected = false; 133 | _connections.splice(_connections.indexOf(this), 1); 134 | }; 135 | 136 | /** 137 | * Sends an Error message 138 | * @param {object} message Error message. Must be JSON.stringify-able. 139 | */ 140 | Connection.prototype.sendError = function (message) { 141 | this._send("error", {message: message}); 142 | }; 143 | 144 | /** 145 | * Sends a response to a command execution 146 | * @param {number} id unique ID of the command that was executed. ID is 147 | * generated by the client when the command is issued. 148 | * @param {object|Buffer} response Result of the command execution. Must 149 | * either be JSON.stringify-able or a raw Buffer. In the latter case, 150 | * the result will be sent as a binary response. 151 | */ 152 | Connection.prototype.sendCommandResponse = function (id, response) { 153 | if (Buffer.isBuffer(response)) { 154 | // Assume the id is an unsigned 32-bit integer, which is encoded 155 | // as a four-byte header 156 | var header = new Buffer(4); 157 | 158 | header.writeUInt32LE(id, 0); 159 | 160 | // Prepend the header to the message 161 | var message = Buffer.concat([header, response], response.length + 4); 162 | 163 | this._sendBinary(message); 164 | } else { 165 | this._send("commandResponse", {id: id, response: response }); 166 | } 167 | }; 168 | 169 | /** 170 | * Sends a response indicating that an error occurred during command 171 | * execution 172 | * @param {number} id unique ID of the command that was executed. ID is 173 | * generated by the client when the command is issued. 174 | * @param {string} message Error message 175 | * @param {?object} stack Call stack from the exception, if possible. Must 176 | * be JSON.stringify-able. 177 | */ 178 | Connection.prototype.sendCommandError = function (id, message, stack) { 179 | this._send("commandError", {id: id, message: message, stack: stack}); 180 | }; 181 | 182 | /** 183 | * Sends an event message 184 | * @param {number} id unique ID for the event. 185 | * @param {string} domain Domain of the event. 186 | * @param {string} event Name of the event 187 | * @param {object} parameters Event parameters. Must be JSON.stringify-able. 188 | */ 189 | Connection.prototype.sendEventMessage = 190 | function (id, domain, event, parameters) { 191 | this._send("event", {id: id, 192 | domain: domain, 193 | event: event, 194 | parameters: parameters 195 | }); 196 | }; 197 | 198 | /** 199 | * Factory function for creating a new Connection 200 | * @param {WebSocket} ws The WebSocket connected to the client. 201 | */ 202 | function createConnection(ws) { 203 | _connections.push(new Connection(ws)); 204 | } 205 | 206 | /** 207 | * Closes all connections gracefully. Should be called during shutdown. 208 | */ 209 | function closeAllConnections() { 210 | var i; 211 | for (i = 0; i < _connections.length; i++) { 212 | try { 213 | _connections[i].close(); 214 | } catch (err) { } 215 | } 216 | _connections = []; 217 | } 218 | 219 | /** 220 | * Sends all open connections the specified event 221 | * @param {number} id unique ID for the event. 222 | * @param {string} domain Domain of the event. 223 | * @param {string} event Name of the event 224 | * @param {object} parameters Event parameters. Must be JSON.stringify-able. 225 | */ 226 | function sendEventToAllConnections(id, domain, event, parameters) { 227 | _connections.forEach(function (c) { 228 | c.sendEventMessage(id, domain, event, parameters); 229 | }); 230 | } 231 | 232 | exports.createConnection = createConnection; 233 | exports.closeAllConnections = closeAllConnections; 234 | exports.sendEventToAllConnections = sendEventToAllConnections; 235 | }()); 236 | -------------------------------------------------------------------------------- /lib/domains/DomainManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | (function () { 25 | "use strict"; 26 | 27 | var util = require("util"), 28 | domain = require("domain"), 29 | ConnectionManager = require("./ConnectionManager"); 30 | 31 | /** 32 | * @constructor 33 | * DomainManager is a module/class that handles the loading, registration, 34 | * and execution of all commands and events. It is a singleton, and is passed 35 | * to a domain in its init() method. 36 | */ 37 | var self = exports; 38 | 39 | /** 40 | * @private 41 | * @type {object} 42 | * Map of all the registered domains 43 | */ 44 | var _domains = {}; 45 | 46 | /** 47 | * @private 48 | * @type {Array.} 49 | * Array of all modules we have loaded. Used for avoiding duplicate loading. 50 | */ 51 | var _initializedDomainModules = []; 52 | 53 | /** 54 | * @private 55 | * @type {number} 56 | * Used for generating unique IDs for events. 57 | */ 58 | var _eventCount = 1; 59 | 60 | /** 61 | * @private 62 | * @type {Array} 63 | * JSON.stringify-able Array of the current API. In the format of 64 | * Inspector.json. This is a cache that we invalidate every time the 65 | * API changes. 66 | */ 67 | var _cachedDomainDescriptions = null; 68 | 69 | /** 70 | * Returns whether a domain with the specified name exists or not. 71 | * @param {string} domainName The domain name. 72 | * @return {boolean} Whether the domain exists 73 | */ 74 | function hasDomain(domainName) { 75 | return !!_domains[domainName]; 76 | } 77 | 78 | /** 79 | * Returns a new empty domain. Throws error if the domain already exists. 80 | * @param {string} domainName The domain name. 81 | * @param {{major: number, minor: number}} version The domain version. 82 | * The version has a format like {major: 1, minor: 2}. It is reported 83 | * in the API spec, but serves no other purpose on the server. The client 84 | * can make use of this. 85 | */ 86 | function registerDomain(domainName, version) { 87 | if (!hasDomain(domainName)) { 88 | // invalidate the cache 89 | _cachedDomainDescriptions = null; 90 | 91 | _domains[domainName] = {version: version, commands: {}, events: {}}; 92 | } else { 93 | console.error("[DomainManager] Domain " + domainName + " already registered"); 94 | } 95 | } 96 | 97 | /** 98 | * Registers a new command with the specified domain. If the domain does 99 | * not yet exist, it registers the domain with a null version. 100 | * @param {string} domainName The domain name. 101 | * @param {string} commandName The command name. 102 | * @param {Function} commandFunction The callback handler for the function. 103 | * The function is called with the arguments specified by the client in the 104 | * command message. Additionally, if the command is asynchronous (isAsync 105 | * parameter is true), the function is called with an automatically- 106 | * constructed callback function of the form cb(err, result). The function 107 | * can then use this to send a response to the client asynchronously. 108 | * @param {boolean} isAsync See explanation for commandFunction param 109 | * @param {?string} description Used in the API documentation 110 | * @param {?Array.<{name: string, type: string, description:string}>} parameters 111 | * Used in the API documentation. 112 | * @param {?Array.<{name: string, type: string, description:string}>} returns 113 | * Used in the API documentation. 114 | */ 115 | function registerCommand(domainName, commandName, commandFunction, isAsync, 116 | description, parameters, returns) { 117 | // invalidate the cache 118 | _cachedDomainDescriptions = null; 119 | 120 | if (!hasDomain(domainName)) { 121 | registerDomain(domainName, null); 122 | } 123 | 124 | if (!_domains[domainName].commands[commandName]) { 125 | _domains[domainName].commands[commandName] = { 126 | commandFunction: commandFunction, 127 | isAsync: isAsync, 128 | description: description, 129 | parameters: parameters, 130 | returns: returns 131 | }; 132 | } else { 133 | throw new Error("Command " + domainName + "." + 134 | commandName + " already registered"); 135 | } 136 | } 137 | 138 | /** 139 | * Executes a command by domain name and command name. Called by a connection's 140 | * message parser. Sends response or error (possibly asynchronously) to the 141 | * connection. 142 | * @param {Connection} connection The requesting connection object. 143 | * @param {number} id The unique command ID. 144 | * @param {string} domainName The domain name. 145 | * @param {string} commandName The command name. 146 | * @param {Array} parameters The parameters to pass to the command function. If 147 | * the command is asynchronous, will be augmented with a callback function. 148 | * (see description in registerCommand documentation) 149 | */ 150 | function executeCommand(connection, id, domainName, 151 | commandName, parameters) { 152 | var el, i; 153 | 154 | for (i = 0; i < parameters.length; i++) { 155 | el = parameters[i]; 156 | if (typeof el === "string") { 157 | if (el.startsWith("/projects/")) { 158 | parameters[i] = exports.projectsDir + el.substr("/projects".length); 159 | } else if (el.startsWith("/samples/")) { 160 | parameters[i] = exports.samplesDir + el.substr("/samples".length); 161 | } 162 | } 163 | } 164 | 165 | if (_domains[domainName] && 166 | _domains[domainName].commands[commandName]) { 167 | var command = _domains[domainName].commands[commandName]; 168 | if (command.isAsync) { 169 | var execDom = domain.create(), 170 | callback = function (err, result) { 171 | if (err) { 172 | connection.sendCommandError(id, err); 173 | } else { 174 | connection.sendCommandResponse(id, result); 175 | } 176 | }; 177 | 178 | parameters.push(callback); 179 | 180 | execDom.on("error", function(err) { 181 | connection.sendCommandError(id, err.message); 182 | execDom.dispose(); 183 | }); 184 | 185 | execDom.bind(command.commandFunction).apply(connection, parameters); 186 | } else { // synchronous command 187 | try { 188 | connection.sendCommandResponse( 189 | id, 190 | command.commandFunction.apply(connection, parameters) 191 | ); 192 | } catch (e) { 193 | connection.sendCommandError(id, e.message); 194 | } 195 | } 196 | } else { 197 | connection.sendCommandError(id, "no such command: " + 198 | domainName + "." + commandName); 199 | } 200 | } 201 | 202 | /** 203 | * Registers an event domain and name. 204 | * @param {string} domainName The domain name. 205 | * @param {string} eventName The event name. 206 | * @param {?Array.<{name: string, type: string, description:string}>} parameters 207 | * Used in the API documentation. 208 | */ 209 | function registerEvent(domainName, eventName, parameters) { 210 | // invalidate the cache 211 | _cachedDomainDescriptions = null; 212 | 213 | if (!hasDomain(domainName)) { 214 | registerDomain(domainName, null); 215 | } 216 | 217 | if (!_domains[domainName].events[eventName]) { 218 | _domains[domainName].events[eventName] = { 219 | parameters: parameters 220 | }; 221 | } else { 222 | console.error("[DomainManager] Event " + domainName + "." + 223 | eventName + " already registered"); 224 | } 225 | } 226 | 227 | /** 228 | * Emits an event with the specified name and parameters to all connections. 229 | * 230 | * TODO: Future: Potentially allow individual connections to register 231 | * for which events they want to receive. Right now, we have so few events 232 | * that it's fine to just send all events to everyone and decide on the 233 | * client side if the client wants to handle them. 234 | * 235 | * @param {string} domainName The domain name. 236 | * @param {string} eventName The event name. 237 | * @param {?Array} parameters The parameters. Must be JSON.stringify-able 238 | */ 239 | function emitEvent(domainName, eventName, parameters) { 240 | if (_domains[domainName] && _domains[domainName].events[eventName]) { 241 | ConnectionManager.sendEventToAllConnections( 242 | _eventCount++, 243 | domainName, 244 | eventName, 245 | parameters 246 | ); 247 | } else { 248 | console.error("[DomainManager] No such event: " + domainName + 249 | "." + eventName); 250 | } 251 | } 252 | 253 | /** 254 | * Loads and initializes domain modules using the specified paths. Checks to 255 | * make sure that a module is not loaded/initialized more than once. 256 | * 257 | * @param {Array.} paths The paths to load. The paths can be relative 258 | * to the DomainManager or absolute. However, modules that aren't in core 259 | * won't know where the DomainManager module is, so in general, all paths 260 | * should be absolute. 261 | * @return {boolean} Whether loading succeded. (Failure will throw an exception). 262 | */ 263 | function loadDomainModulesFromPaths(paths) { 264 | var pathArray = paths; 265 | if (!util.isArray(paths)) { 266 | pathArray = [paths]; 267 | } 268 | pathArray.forEach(function (path) { 269 | if (path.startsWith(exports.httpRoot)) { 270 | path = "../../brackets-srv" + path.substr(exports.httpRoot.length); 271 | } else if (path.startsWith("/support/extensions/user/")) { 272 | if (exports.allowUserDomains) { 273 | path = exports.supportDir + path.substr("/support".length); 274 | } else { 275 | console.error("ERROR: User domains are not allowed: " + path); 276 | return false; 277 | } 278 | } else if (path !== "./BaseDomain") { 279 | console.error("ERROR: Invalid domain path: " + path); 280 | return false; 281 | } 282 | 283 | try { 284 | var m = require(path); 285 | if (m && m.init && _initializedDomainModules.indexOf(m) < 0) { 286 | m.init(self); 287 | _initializedDomainModules.push(m); // don't init more than once 288 | } 289 | } catch (err) { 290 | console.error(err); 291 | return false; 292 | } 293 | }); 294 | return true; // if we fail, an exception will be thrown 295 | } 296 | 297 | /** 298 | * Returns a description of all registered domains in the format of WebKit's 299 | * Inspector.json. Used for sending API documentation to clients. 300 | * 301 | * @return {Array} Array describing all domains. 302 | */ 303 | function getDomainDescriptions() { 304 | if (!_cachedDomainDescriptions) { 305 | _cachedDomainDescriptions = []; 306 | 307 | var domainNames = Object.keys(_domains); 308 | domainNames.forEach(function (domainName) { 309 | var d = { 310 | domain: domainName, 311 | version: _domains[domainName].version, 312 | commands: [], 313 | events: [] 314 | }; 315 | var commandNames = Object.keys(_domains[domainName].commands); 316 | commandNames.forEach(function (commandName) { 317 | var c = _domains[domainName].commands[commandName]; 318 | d.commands.push({ 319 | name: commandName, 320 | description: c.description, 321 | parameters: c.parameters, 322 | returns: c.returns 323 | }); 324 | }); 325 | var eventNames = Object.keys(_domains[domainName].events); 326 | eventNames.forEach(function (eventName) { 327 | d.events.push({ 328 | name: eventName, 329 | parameters: _domains[domainName].events[eventName].parameters 330 | }); 331 | }); 332 | _cachedDomainDescriptions.push(d); 333 | }); 334 | } 335 | return _cachedDomainDescriptions; 336 | } 337 | 338 | exports.hasDomain = hasDomain; 339 | exports.registerDomain = registerDomain; 340 | exports.registerCommand = registerCommand; 341 | exports.executeCommand = executeCommand; 342 | exports.registerEvent = registerEvent; 343 | exports.emitEvent = emitEvent; 344 | exports.loadDomainModulesFromPaths = loadDomainModulesFromPaths; 345 | exports.getDomainDescriptions = getDomainDescriptions; 346 | }()); 347 | -------------------------------------------------------------------------------- /hacks/NodeConnection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | 25 | /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, 26 | maxerr: 50, browser: true */ 27 | /*global $, define, brackets, WebSocket, ArrayBuffer, Uint32Array */ 28 | 29 | define(function (require, exports, module) { 30 | "use strict"; 31 | 32 | var EventDispatcher = require("utils/EventDispatcher"); 33 | 34 | /** 35 | * Connection attempts to make before failing 36 | * @type {number} 37 | */ 38 | var CONNECTION_ATTEMPTS = 10; 39 | 40 | /** 41 | * Milliseconds to wait before a particular connection attempt is considered failed. 42 | * NOTE: It's okay for the connection timeout to be long because the 43 | * expected behavior of WebSockets is to send a "close" event as soon 44 | * as they realize they can't connect. So, we should rarely hit the 45 | * connection timeout even if we try to connect to a port that isn't open. 46 | * @type {number} 47 | */ 48 | var CONNECTION_TIMEOUT = 10000; // 10 seconds 49 | 50 | /** 51 | * Milliseconds to wait before retrying connecting 52 | * @type {number} 53 | */ 54 | var RETRY_DELAY = 500; // 1/2 second 55 | 56 | /** 57 | * Maximum value of the command ID counter 58 | * @type {number} 59 | */ 60 | var MAX_COUNTER_VALUE = 4294967295; // 2^32 - 1 61 | 62 | /** 63 | * @private 64 | * Helper function to auto-reject a deferred after a given amount of time. 65 | * If the deferred is resolved/rejected manually, then the timeout is 66 | * automatically cleared. 67 | */ 68 | function setDeferredTimeout(deferred, delay) { 69 | var timer = setTimeout(function () { 70 | deferred.reject("timeout"); 71 | }, delay); 72 | deferred.always(function () { clearTimeout(timer); }); 73 | } 74 | 75 | /** 76 | * @private 77 | * Helper function to attempt a single connection to the node server 78 | */ 79 | function attemptSingleConnect() { 80 | var deferred = $.Deferred(); 81 | var port = null; 82 | var ws = null; 83 | setDeferredTimeout(deferred, CONNECTION_TIMEOUT); 84 | 85 | brackets.app.getNodeState(function (err, nodePort) { 86 | if (!err && nodePort && deferred.state() !== "rejected") { 87 | port = nodePort; 88 | //ws = new WebSocket("ws://" + window.location.host + "/brackets-ext"); 89 | ws = require("socket.io").connect("/brackets-ext"); 90 | if (ws.connected) { 91 | deferred.resolveWith(null, [ws, port]); 92 | } else { 93 | ws.on("connect", function () { 94 | console.log("Socket on /brackets-ext connected!"); 95 | deferred.resolveWith(null, [ws, port]); 96 | }); 97 | 98 | ws.on("error", function (data) { 99 | deferred.reject(data); 100 | }); 101 | } 102 | 103 | // Expect ArrayBuffer objects from Node when receiving binary 104 | // data instead of DOM Blobs, which are the default. 105 | // ws.binaryType = "arraybuffer"; 106 | 107 | // If the server port isn't open, we get a close event 108 | // at some point in the future (and will not get an onopen 109 | // event) 110 | // ws.onclose = function () { 111 | // deferred.reject("WebSocket closed"); 112 | // }; 113 | 114 | // ws.onopen = function () { 115 | // // If we successfully opened, remove the old onclose 116 | // // handler (which was present to detect failure to 117 | // // connect at all). 118 | // ws.onclose = null; 119 | // deferred.resolveWith(null, [ws, port]); 120 | // }; 121 | } else { 122 | deferred.reject("brackets.app.getNodeState error: " + err); 123 | } 124 | }); 125 | 126 | return deferred.promise(); 127 | } 128 | 129 | /** 130 | * Provides an interface for interacting with the node server. 131 | * @constructor 132 | */ 133 | function NodeConnection() { 134 | this.domains = {}; 135 | this._registeredModules = []; 136 | this._pendingInterfaceRefreshDeferreds = []; 137 | this._pendingCommandDeferreds = []; 138 | } 139 | 140 | EventDispatcher.makeEventDispatcher(NodeConnection.prototype); 141 | 142 | /** 143 | * @type {Object} 144 | * Exposes the domains registered with the server. This object will 145 | * have a property for each registered domain. Each of those properties 146 | * will be an object containing properties for all the commands in that 147 | * domain. So, myConnection.base.enableDebugger would point to the function 148 | * to call to enable the debugger. 149 | * 150 | * This object is automatically replaced every time the API changes (based 151 | * on the base:newDomains event from the server). Therefore, code that 152 | * uses this object should not keep their own pointer to the domain property. 153 | */ 154 | NodeConnection.prototype.domains = null; 155 | 156 | /** 157 | * @private 158 | * @type {Array.} 159 | * List of module pathnames that should be re-registered if there is 160 | * a disconnection/connection (i.e. if the server died). 161 | */ 162 | NodeConnection.prototype._registeredModules = null; 163 | 164 | /** 165 | * @private 166 | * @type {WebSocket} 167 | * The connection to the server 168 | */ 169 | NodeConnection.prototype._ws = null; 170 | 171 | /** 172 | * @private 173 | * @type {?number} 174 | * The port the WebSocket is currently connected to 175 | */ 176 | NodeConnection.prototype._port = null; 177 | 178 | /** 179 | * @private 180 | * @type {number} 181 | * Unique ID for commands 182 | */ 183 | NodeConnection.prototype._commandCount = 1; 184 | 185 | /** 186 | * @private 187 | * @type {boolean} 188 | * Whether to attempt reconnection if connection fails 189 | */ 190 | NodeConnection.prototype._autoReconnect = false; 191 | 192 | /** 193 | * @private 194 | * @type {Array.} 195 | * List of deferred objects that should be resolved pending 196 | * a successful refresh of the API 197 | */ 198 | NodeConnection.prototype._pendingInterfaceRefreshDeferreds = null; 199 | 200 | /** 201 | * @private 202 | * @type {Array.} 203 | * Array (indexed on command ID) of deferred objects that should be 204 | * resolved/rejected with the response of commands. 205 | */ 206 | NodeConnection.prototype._pendingCommandDeferreds = null; 207 | 208 | /** 209 | * @private 210 | * @return {number} The next command ID to use. Always representable as an 211 | * unsigned 32-bit integer. 212 | */ 213 | NodeConnection.prototype._getNextCommandID = function () { 214 | var nextID; 215 | 216 | if (this._commandCount > MAX_COUNTER_VALUE) { 217 | nextID = this._commandCount = 0; 218 | } else { 219 | nextID = this._commandCount++; 220 | } 221 | 222 | return nextID; 223 | }; 224 | 225 | /** 226 | * @private 227 | * Helper function to do cleanup work when a connection fails 228 | */ 229 | NodeConnection.prototype._cleanup = function () { 230 | // clear out the domains, since we may get different ones 231 | // on the next connection 232 | this.domains = {}; 233 | 234 | // shut down the old connection if there is one 235 | if (this._ws && this._ws.connected) { 236 | try { 237 | this._ws.disconnect(); 238 | } catch (e) { } 239 | } 240 | var failedDeferreds = this._pendingInterfaceRefreshDeferreds 241 | .concat(this._pendingCommandDeferreds); 242 | failedDeferreds.forEach(function (d) { 243 | d.reject("cleanup"); 244 | }); 245 | this._pendingInterfaceRefreshDeferreds = []; 246 | this._pendingCommandDeferreds = []; 247 | 248 | this._ws = null; 249 | this._port = null; 250 | }; 251 | 252 | /** 253 | * Connect to the node server. After connecting, the NodeConnection 254 | * object will trigger a "close" event when the underlying socket 255 | * is closed. If the connection is set to autoReconnect, then the 256 | * event will also include a jQuery promise for the connection. 257 | * 258 | * @param {boolean} autoReconnect Whether to automatically try to 259 | * reconnect to the server if the connection succeeds and then 260 | * later disconnects. Note if this connection fails initially, the 261 | * autoReconnect flag is set to false. Future calls to connect() 262 | * can reset it to true 263 | * @return {jQuery.Promise} Promise that resolves/rejects when the 264 | * connection succeeds/fails 265 | */ 266 | NodeConnection.prototype.connect = function (autoReconnect) { 267 | var self = this; 268 | self._autoReconnect = autoReconnect; 269 | var deferred = $.Deferred(); 270 | var attemptCount = 0; 271 | var attemptTimestamp = null; 272 | 273 | // Called after a successful connection to do final setup steps 274 | function registerHandlersAndDomains(ws, port) { 275 | // Called if we succeed at the final setup 276 | function success() { 277 | self._ws.onclose = function () { 278 | if (self._autoReconnect) { 279 | var $promise = self.connect(true); 280 | self.trigger("close", $promise); 281 | } else { 282 | self._cleanup(); 283 | self.trigger("close"); 284 | } 285 | }; 286 | deferred.resolve(); 287 | } 288 | // Called if we fail at the final setup 289 | function fail(err) { 290 | self._cleanup(); 291 | deferred.reject(err); 292 | } 293 | 294 | self._ws = ws; 295 | self._port = port; 296 | self._ws.on("message", self._receive.bind(self)); 297 | 298 | // refresh the current domains, then re-register any 299 | // "autoregister" modules 300 | self._refreshInterface().then( 301 | function () { 302 | if (self._registeredModules.length > 0) { 303 | self.loadDomains(self._registeredModules, false).then( 304 | success, 305 | fail 306 | ); 307 | } else { 308 | success(); 309 | } 310 | }, 311 | fail 312 | ); 313 | } 314 | 315 | // Repeatedly tries to connect until we succeed or until we've 316 | // failed CONNECTION_ATTEMPT times. After each attempt, waits 317 | // at least RETRY_DELAY before trying again. 318 | function doConnect() { 319 | attemptCount++; 320 | attemptTimestamp = new Date(); 321 | attemptSingleConnect().then( 322 | registerHandlersAndDomains, // succeded 323 | function () { // failed this attempt, possibly try again 324 | if (attemptCount < CONNECTION_ATTEMPTS) { //try again 325 | // Calculate how long we should wait before trying again 326 | var now = new Date(); 327 | var delay = Math.max( 328 | RETRY_DELAY - (now - attemptTimestamp), 329 | 1 330 | ); 331 | setTimeout(doConnect, delay); 332 | } else { // too many attempts, give up 333 | deferred.reject("Max connection attempts reached"); 334 | } 335 | } 336 | ); 337 | } 338 | 339 | // Start the connection process 340 | self._cleanup(); 341 | doConnect(); 342 | 343 | return deferred.promise(); 344 | }; 345 | 346 | /** 347 | * Determines whether the NodeConnection is currently connected 348 | * @return {boolean} Whether the NodeConnection is connected. 349 | */ 350 | NodeConnection.prototype.connected = function () { 351 | return !!(this._ws && this._ws.connected); 352 | }; 353 | 354 | /** 355 | * Explicitly disconnects from the server. Note that even if 356 | * autoReconnect was set to true at connection time, the connection 357 | * will not reconnect after this call. Reconnection can be manually done 358 | * by calling connect() again. 359 | */ 360 | NodeConnection.prototype.disconnect = function () { 361 | this._autoReconnect = false; 362 | this._cleanup(); 363 | }; 364 | 365 | /** 366 | * Load domains into the server by path 367 | * @param {Array.} List of absolute paths to load 368 | * @param {boolean} autoReload Whether to auto-reload the domains if the server 369 | * fails and restarts. Note that the reload is initiated by the 370 | * client, so it will only happen after the client reconnects. 371 | * @return {jQuery.Promise} Promise that resolves after the load has 372 | * succeeded and the new API is availale at NodeConnection.domains, 373 | * or that rejects on failure. 374 | */ 375 | NodeConnection.prototype.loadDomains = function (paths, autoReload) { 376 | var deferred = $.Deferred(); 377 | setDeferredTimeout(deferred, CONNECTION_TIMEOUT); 378 | var pathArray = paths; 379 | if (!Array.isArray(paths)) { 380 | pathArray = [paths]; 381 | } 382 | 383 | if (autoReload) { 384 | Array.prototype.push.apply(this._registeredModules, pathArray); 385 | } 386 | 387 | if (this.domains.base && this.domains.base.loadDomainModulesFromPaths) { 388 | this.domains.base.loadDomainModulesFromPaths(pathArray).then( 389 | function (success) { // command call succeeded 390 | if (!success) { 391 | // response from commmand call was "false" so we know 392 | // the actual load failed. 393 | deferred.reject("loadDomainModulesFromPaths failed"); 394 | } 395 | // if the load succeeded, we wait for the API refresh to 396 | // resolve the deferred. 397 | }, 398 | function (reason) { // command call failed 399 | deferred.reject("Unable to load one of the modules: " + pathArray + (reason ? ", reason: " + reason : "")); 400 | } 401 | ); 402 | 403 | this._pendingInterfaceRefreshDeferreds.push(deferred); 404 | } else { 405 | deferred.reject("this.domains.base is undefined"); 406 | } 407 | 408 | return deferred.promise(); 409 | }; 410 | 411 | /** 412 | * @private 413 | * Sends a message over the WebSocket. Automatically JSON.stringifys 414 | * the message if necessary. 415 | * @param {Object|string} m Object to send. Must be JSON.stringify-able. 416 | */ 417 | NodeConnection.prototype._send = function (m) { 418 | if (this.connected()) { 419 | 420 | // Convert the message to a string 421 | var messageString = null; 422 | if (typeof m === "string") { 423 | messageString = m; 424 | } else { 425 | try { 426 | messageString = JSON.stringify(m); 427 | } catch (stringifyError) { 428 | console.error("[NodeConnection] Unable to stringify message in order to send: " + stringifyError.message); 429 | } 430 | } 431 | 432 | // If we succeded in making a string, try to send it 433 | if (messageString) { 434 | try { 435 | this._ws.send(messageString); 436 | } catch (sendError) { 437 | console.error("[NodeConnection] Error sending message: " + sendError.message); 438 | } 439 | } 440 | } else { 441 | console.error("[NodeConnection] Not connected to node, unable to send."); 442 | } 443 | }; 444 | 445 | /** 446 | * @private 447 | * Handler for receiving events on the WebSocket. Parses the message 448 | * and dispatches it appropriately. 449 | * @param {WebSocket.Message} message Message object from WebSocket 450 | */ 451 | NodeConnection.prototype._receive = function (message) { 452 | var responseDeferred = null; 453 | var data = message; 454 | var m; 455 | 456 | if (message instanceof ArrayBuffer) { 457 | // The first four bytes encode the command ID as an unsigned 32-bit integer 458 | if (data.byteLength < 4) { 459 | console.error("[NodeConnection] received malformed binary message"); 460 | return; 461 | } 462 | 463 | var header = data.slice(0, 4), 464 | body = data.slice(4), 465 | headerView = new Uint32Array(header), 466 | id = headerView[0]; 467 | 468 | // Unpack the binary message into a commandResponse 469 | m = { 470 | type: "commandResponse", 471 | message: { 472 | id: id, 473 | response: body 474 | } 475 | }; 476 | } else { 477 | try { 478 | m = JSON.parse(data); 479 | } catch (e) { 480 | console.error("[NodeConnection] received malformed message", message, e.message); 481 | return; 482 | } 483 | } 484 | 485 | switch (m.type) { 486 | case "event": 487 | 488 | if (m.message.domain === "base" && m.message.event === "newDomains") { 489 | this._refreshInterface(); 490 | } 491 | 492 | // Event type "domain:event" 493 | EventDispatcher.triggerWithArray(this, m.message.domain + ":" + m.message.event, 494 | + m.message.parameters); 495 | break; 496 | case "commandResponse": 497 | responseDeferred = this._pendingCommandDeferreds[m.message.id]; 498 | if (responseDeferred) { 499 | responseDeferred.resolveWith(this, [m.message.response]); 500 | delete this._pendingCommandDeferreds[m.message.id]; 501 | } 502 | break; 503 | case "commandError": 504 | responseDeferred = this._pendingCommandDeferreds[m.message.id]; 505 | if (responseDeferred) { 506 | responseDeferred.rejectWith( 507 | this, 508 | [m.message.message, m.message.stack] 509 | ); 510 | delete this._pendingCommandDeferreds[m.message.id]; 511 | } 512 | break; 513 | case "error": 514 | console.error("[NodeConnection] received error: " + 515 | m.message.message); 516 | break; 517 | default: 518 | console.error("[NodeConnection] unknown event type: " + m.type); 519 | } 520 | }; 521 | 522 | /** 523 | * @private 524 | * Helper function for refreshing the interface in the "domain" property. 525 | * Automatically called when the connection receives a base:newDomains 526 | * event from the server, and also called at connection time. 527 | */ 528 | NodeConnection.prototype._refreshInterface = function () { 529 | var deferred = $.Deferred(); 530 | var self = this; 531 | 532 | var pendingDeferreds = this._pendingInterfaceRefreshDeferreds; 533 | this._pendingInterfaceRefreshDeferreds = []; 534 | deferred.then( 535 | function () { 536 | pendingDeferreds.forEach(function (d) { d.resolve(); }); 537 | }, 538 | function (err) { 539 | pendingDeferreds.forEach(function (d) { d.reject(err); }); 540 | } 541 | ); 542 | 543 | function refreshInterfaceCallback(spec) { 544 | function makeCommandFunction(domainName, commandSpec) { 545 | return function () { 546 | var deferred = $.Deferred(); 547 | var parameters = Array.prototype.slice.call(arguments, 0); 548 | var id = self._getNextCommandID(); 549 | self._pendingCommandDeferreds[id] = deferred; 550 | self._send({id: id, 551 | domain: domainName, 552 | command: commandSpec.name, 553 | parameters: parameters 554 | }); 555 | return deferred; 556 | }; 557 | } 558 | 559 | // TODO: Don't replace the domain object every time. Instead, merge. 560 | self.domains = {}; 561 | self.domainEvents = {}; 562 | spec.forEach(function (domainSpec) { 563 | self.domains[domainSpec.domain] = {}; 564 | domainSpec.commands.forEach(function (commandSpec) { 565 | self.domains[domainSpec.domain][commandSpec.name] = 566 | makeCommandFunction(domainSpec.domain, commandSpec); 567 | }); 568 | self.domainEvents[domainSpec.domain] = {}; 569 | domainSpec.events.forEach(function (eventSpec) { 570 | var parameters = eventSpec.parameters; 571 | self.domainEvents[domainSpec.domain][eventSpec.name] = parameters; 572 | }); 573 | }); 574 | deferred.resolve(); 575 | } 576 | 577 | if (this.connected()) { 578 | $.getJSON("/brackets-ext/api") 579 | .done(refreshInterfaceCallback) 580 | .fail(function (err) { deferred.reject(err); }); 581 | } else { 582 | deferred.reject("Attempted to call _refreshInterface when not connected."); 583 | } 584 | 585 | return deferred.promise(); 586 | }; 587 | 588 | /** 589 | * @private 590 | * Get the default timeout value 591 | * @return {number} Timeout value in milliseconds 592 | */ 593 | NodeConnection._getConnectionTimeout = function () { 594 | return CONNECTION_TIMEOUT; 595 | }; 596 | 597 | module.exports = NodeConnection; 598 | 599 | }); 600 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*jshint -W106 */ 2 | 3 | "use strict"; 4 | 5 | var fs = require("fs"), 6 | path = require("path"), 7 | glob = require("glob"), 8 | shell = require("shelljs"), 9 | _replace = { 10 | // HACK: 1. We have to mock shell app. 11 | // HACK: 2. Brackets inBrowser behaves very differently, that's why we have to fake it. 12 | // HACK: 3. We need the menus in the Browser. 13 | // HACK: 4/5. Brackets extension registry services don't allow CORS, that's why we have to proxy the requests. 14 | "utils/Global": { 15 | match: "global.brackets.app = {};", 16 | value: "global.brackets.app=require(\"hacks.app\");global.brackets.inBrowser=false; global.brackets.nativeMenus=false;global.brackets.fs=require(\"hacks.lowFs\");" 17 | }, 18 | // HACK: Remove warning dialog about Brackets not been ready for browsers. 19 | "brackets": [ 20 | { 21 | match: /\/\/ Let the user know Brackets doesn't run in a web browser yet\s+if \(brackets.inBrowser\) {/, 22 | value: "if (false) {" 23 | }, 24 | { 25 | match: "!url.match(/^file:\\/\\//) && url !== \"about:blank\" && url.indexOf(\":\") !== -1", 26 | varlue: "false" 27 | } 28 | ], 29 | // TODO:HACK: For some reason this line causes languageDropdown to be populated before it si initialized. Needs more investigaton. 30 | "editor/EditorStatusBar": { 31 | match: "$(LanguageManager).on(\"languageAdded languageModified\", _populateLanguageDropdown);", 32 | value: "// $(LanguageManager).on(\"languageAdded languageModified\", _populateLanguageDropdown);" 33 | }, 34 | "command/DefaultMenus": [ 35 | { 36 | // Browser window cannot be closed from script. 37 | match: "if (brackets.platform !== \"mac\" || !brackets.nativeMenus) {", 38 | value: "if (false) {" 39 | }, 40 | { 41 | match: /menu\.addMenuDivider\(\);\s*menu\.addMenuItem\(Commands.HELP_SHOW_EXT_FOLDER\);/, 42 | value: " " 43 | } 44 | ] 45 | }; 46 | 47 | function addCodeMirrorModes(config) { 48 | var root = path.join(__dirname, "brackets-src", "src", "thirdparty", "CodeMirror", "mode"), 49 | dirs = fs.readdirSync(root), 50 | include = config.requirejs.main.options.include; 51 | 52 | dirs.forEach(function (file) { 53 | var stat = fs.statSync(root + "/" + file); 54 | if (stat.isDirectory()) { 55 | include.push("thirdparty/CodeMirror/mode/" + file + "/" + file); 56 | } 57 | }); 58 | } 59 | 60 | //function addCodeMirrorFold(config) { 61 | // var root = path.join(__dirname, "brackets-src", "src", "thirdparty", "CodeMirror2", "addon", "fold"), 62 | // dirs = fs.readdirSync(root), 63 | // include = config.requirejs.main.options.include; 64 | // 65 | // dirs.forEach(function (file) { 66 | // if (path.extname(file) === ".js") { 67 | // include.push("thirdparty/CodeMirror2/addon/fold/" + file); 68 | // } 69 | // }); 70 | //} 71 | 72 | function addDefaultExtesions(config) { 73 | var root = path.join(__dirname, "brackets-src", "src", "extensions", "default"), 74 | dirs = fs.readdirSync(root), 75 | rj = config.requirejs; 76 | 77 | dirs.forEach(function (file) { 78 | var stat = fs.statSync(root + "/" + file); 79 | 80 | // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 81 | // TODO: JavaScriptCodeHints cannot be optimized for multiple problems. Needs more investigation. 82 | // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 83 | if (stat.isDirectory() && fs.existsSync(root + "/" + file + "/main.js") && file !== "JavaScriptCodeHints") { 84 | var mod = { 85 | options: { 86 | name: "main", 87 | out: "brackets-dist/extensions/default/" + file + "/main.js", 88 | baseUrl: "brackets-src/src/extensions/default/" + file + "/", 89 | preserveLicenseComments: false, 90 | optimize: "uglify2", 91 | uglify2: {}, 92 | paths: { 93 | "text" : "../../../thirdparty/text/text", 94 | "i18n" : "../../../thirdparty/i18n/i18n", 95 | }, 96 | generateSourceMaps: true, 97 | useSourceUrl: true, 98 | wrap: false 99 | } 100 | }; 101 | 102 | // HealtData extension is placeing its menu item after HELP_SHOW_EXT_FOLDER, but we remove that one and 103 | // that is causing exeception in command manager. 104 | if (file === "HealthData") { 105 | mod.options.onBuildRead = function (moduleName, path, contents) { 106 | if (moduleName === "main") { 107 | return contents.replace(/HELP_SHOW_EXT_FOLDER/g, "HELP_CHECK_FOR_UPDATE"); 108 | } 109 | return contents; 110 | }; 111 | } 112 | 113 | rj[file] = mod; 114 | 115 | // The code below solves some of the problems with JavaScriptCodeHints optimization. 116 | // if (file === "JavaScriptCodeHints") { 117 | // mod.options.onBuildRead = function (moduleName, path, contents) { 118 | // return contents.replace("== \"use strict\"", "== \"use\\ strict\""); 119 | // }; 120 | // 121 | // rj.ternWorker = { 122 | // options: { 123 | // name: "tern-worker", 124 | // out: "brackets-dist/extensions/default/JavaScriptCodeHints/tern-worker.js", 125 | // baseUrl: "brackets-src/src/extensions/default/JavaScriptCodeHints/", 126 | // preserveLicenseComments: false, 127 | // optimize: "uglify2", 128 | // uglify2: {}, 129 | // paths: { 130 | // "text" : "../../../thirdparty/text/text", 131 | // "i18n" : "../../../thirdparty/i18n/i18n", 132 | // }, 133 | // wrap: false 134 | // } 135 | // }; 136 | // } 137 | } 138 | }); 139 | } 140 | 141 | function addEmbeddedExtesions(config) { 142 | var root = path.join(__dirname, "embedded-ext"), 143 | dirs = fs.readdirSync(root), 144 | rj = config.requirejs; 145 | 146 | dirs.forEach(function (file) { 147 | var mod = { 148 | options: { 149 | name: "main", 150 | out: "brackets-dist/extensions/default/" + file + "/main.js", 151 | baseUrl: "embedded-ext/" + file + "/", 152 | preserveLicenseComments: false, 153 | optimize: "uglify2", 154 | uglify2: {}, 155 | paths: { 156 | "text" : "../../brackets-src/src/thirdparty/text/text", 157 | "i18n" : "../../brackets-src/src/thirdparty/i18n/i18n", 158 | }, 159 | generateSourceMaps: true, 160 | useSourceUrl: true, 161 | wrap: false 162 | } 163 | }; 164 | 165 | rj[file] = mod; 166 | }); 167 | } 168 | 169 | module.exports = function (grunt) { 170 | 171 | // load dependencies 172 | require("load-grunt-tasks")(grunt, {pattern: ["grunt-*"]}); 173 | //grunt.loadTasks("tasks"); 174 | 175 | var config = { 176 | jsdoc: { 177 | dist: { 178 | src: ["./server", "README.md"], 179 | options: { 180 | destination: "./cache/docs", 181 | tutorials: "./cache/docs/tutorials", 182 | template: "./node_modules/grunt-jsdoc/node_modules/ink-docstrap/template", 183 | configure: "./jsdoc.json" 184 | } 185 | } 186 | }, 187 | "gh-pages": { 188 | options: { 189 | base: "./cache/docs" 190 | }, 191 | src: ["**"] 192 | }, 193 | simplemocha: { 194 | options: { 195 | timeout: 3000, 196 | ignoreLeaks: false, 197 | reporter: "spec" 198 | }, 199 | all: { 200 | src: ["./test/*_test.js"] 201 | }, 202 | server: { 203 | src: ["./test/server_test.js"] 204 | } 205 | }, 206 | shell: { 207 | debug: { 208 | options: { 209 | stdout: true 210 | }, 211 | command: function (target) { 212 | if (process.platform === "win32") { 213 | return "grunt-debug test:" + target; 214 | } 215 | 216 | return "node --debug-brk $(which grunt) test:" + target; 217 | } 218 | } 219 | }, 220 | concurrent: { 221 | options: { 222 | logConcurrentOutput: true 223 | }, 224 | debug_all: ["node-inspector", "shell:debug:all"], 225 | debug_server: ["node-inspector", "shell:debug:server"] 226 | }, 227 | "node-inspector": { 228 | "default": {} 229 | }, 230 | release: { 231 | options: { 232 | npm: false 233 | } 234 | }, 235 | clean: { 236 | dist: { 237 | files: [{ 238 | dot: true, 239 | src: [ 240 | "brackets-srv", 241 | "brackets-dist", 242 | "brackets-src/src/.index.html", 243 | "brackets-src/src/styles/brackets.css" 244 | ] 245 | }] 246 | } 247 | }, 248 | copy: { 249 | dist: { 250 | files: [ 251 | { 252 | "brackets-dist/index.html": "brackets-src/src/.index.html" 253 | 254 | }, 255 | /* static files */ 256 | { 257 | expand: true, 258 | dest: "brackets-dist/", 259 | cwd: "brackets-src/src/", 260 | src: [ 261 | "nls/{,*/}*.js", 262 | "xorigin.js", 263 | "dependencies.js", 264 | "thirdparty/requirejs/require.js", 265 | "LiveDevelopment/launch.html" 266 | ] 267 | }, 268 | /* extensions and CodeMirror modes */ 269 | { 270 | expand: true, 271 | dest: "brackets-dist/", 272 | cwd: "brackets-src/src/", 273 | src: [ 274 | "extensions/default/JavaScriptCodeHints/**", 275 | "extensions/default/*/**/*.{css,less,json,svg,png}", 276 | "!extensions/default/*/unittest-files/**", 277 | "extensions/dev/*", 278 | "thirdparty/CodeMirror/lib/{,*/}*.css", 279 | "thirdparty/CodeMirror/addon/fold/**", 280 | "thirdparty/i18n/*.js", 281 | "thirdparty/text/*.js" 282 | ] 283 | }, 284 | /* Node domains */ 285 | { 286 | expand: true, 287 | dest: "brackets-srv/", 288 | cwd: "brackets-src/src/", 289 | src: [ 290 | "extensions/default/StaticServer/node/**", 291 | "LiveDevelopment/MultiBrowserImpl/transports/node/**", 292 | "LiveDevelopment/MultiBrowserImpl/launchers/node/**", 293 | "extensibility/node/**", 294 | "!extensibility/node/ExtensionManagerDomain.js", 295 | "search/node/**" 296 | ] //, 297 | // rename: function(dest, src) { 298 | // return dest + src.replace(/node_modules/g, "_node_modules"); 299 | // } 300 | }, 301 | /* Node domains */ 302 | { 303 | expand: true, 304 | dest: "brackets-srv/extensibility/node/", 305 | cwd: "lib/domains/", 306 | src: ["ExtensionManagerDomain.js"] 307 | }, 308 | /* styles, fonts and images */ 309 | { 310 | expand: true, 311 | dest: "brackets-dist/styles", 312 | cwd: "brackets-src/src/styles", 313 | src: ["jsTreeTheme.css", "fonts/{,*/}*.*", "images/*", "brackets.min.css*"] 314 | }, 315 | /* samples */ 316 | { 317 | expand: true, 318 | dest: "brackets-dist/", 319 | cwd: "brackets-src/", 320 | src: [ 321 | "samples/**" 322 | ] 323 | }, 324 | /* embedded extensions */ 325 | { 326 | expand: true, 327 | dest: "brackets-dist/extensions/default/", 328 | cwd: "embedded-ext/", 329 | src: [ 330 | "**", 331 | "!*/main.js" 332 | ] 333 | } 334 | ] 335 | } 336 | }, 337 | less: { 338 | dist: { 339 | files: { 340 | "brackets-src/src/styles/brackets.min.css": "brackets-src/src/styles/brackets.less" 341 | }, 342 | options: { 343 | compress: true, 344 | sourceMap: true, 345 | sourceMapFilename: "brackets-src/src/styles/brackets.min.css.map", 346 | outputSourceFiles: true, 347 | sourceMapRootpath: "", 348 | sourceMapBasepath: "brackets-src/src/styles" 349 | } 350 | } 351 | }, 352 | requirejs: { 353 | main: { 354 | options: { 355 | // `name` and `out` is set by grunt-usemin 356 | name: "main", 357 | out: "brackets-dist/main.js", 358 | mainConfigFile: "brackets-src/src/main.js", 359 | baseUrl: "brackets-src/src", 360 | optimize: "uglify2", 361 | uglify2: {}, // https://github.com/mishoo/UglifyJS2 362 | include: ["utils/Compatibility", "brackets"], 363 | preserveLicenseComments: false, 364 | exclude: ["text!config.json"], 365 | paths: { 366 | "hacks.app": "../../hacks/app", 367 | "hacks.lowFs": "../../hacks/low-level-fs", 368 | "socket.io": "../../node_modules/socket.io-client/socket.io" 369 | }, 370 | onBuildRead: function (moduleName, path, contents) { 371 | var rpl = _replace[moduleName]; 372 | if (rpl) { 373 | if (Array.isArray(rpl)) { 374 | rpl.forEach(function (el) { 375 | contents = contents.replace(el.match, el.value); 376 | }); 377 | return contents; 378 | } 379 | return contents.replace(rpl.match, rpl.value); 380 | } else if (moduleName === "fileSystemImpl") { 381 | // HACK: For in browser loading we need to replace file system implementation very early to avoid exceptions. 382 | return fs.readFileSync(__dirname + "/embedded-ext/client-fs/lib/file-system.js", { encoding: "utf8" }) 383 | .replace(/brackets\.getModule/g, "require") 384 | .replace("require(\"./open-dialog\")", "{}") 385 | .replace("require(\"./save-dialog\")", "{}") 386 | .replace("require(\"../thirdparty/socket.io\");", "require(\"socket.io\");") 387 | .replace("// init(\"/brackets\");", "init(\"/brackets\");"); 388 | } else if (moduleName === "utils/NodeConnection") { 389 | // HACK: We serve the source from Node, connect to the same instance. 390 | return fs.readFileSync(__dirname + "/hacks/NodeConnection.js", { encoding: "utf8" }); 391 | } 392 | return contents; 393 | }, 394 | generateSourceMaps: true, 395 | useSourceUrl: true, 396 | wrap: false 397 | } 398 | } 399 | }, 400 | replace: { 401 | dist: { 402 | src: "brackets-src/src/.index.html", 403 | overwrite: true, 404 | replacements: [{ 405 | from: "", 406 | to: " " 407 | }] 408 | } 409 | }, 410 | targethtml: { 411 | dist: { 412 | files: { 413 | "brackets-src/src/.index.html": "brackets-src/src/index.html" 414 | } 415 | } 416 | }, 417 | useminPrepare: { 418 | options: { 419 | dest: "brackets-dist" 420 | }, 421 | html: "brackets-src/src/.index.html" 422 | }, 423 | usemin: { 424 | options: { 425 | dirs: ["brackets-dist"] 426 | }, 427 | html: ["brackets-dist/{,*/}*.html"] 428 | }, 429 | htmlmin: { 430 | dist: { 431 | options: { 432 | /*removeCommentsFromCDATA: true, 433 | // https://github.com/yeoman/grunt-usemin/issues/44 434 | //collapseWhitespace: true, 435 | collapseBooleanAttributes: true, 436 | removeAttributeQuotes: true, 437 | removeRedundantAttributes: true, 438 | useShortDoctype: true, 439 | removeEmptyAttributes: true, 440 | removeOptionalTags: true*/ 441 | }, 442 | files: [{ 443 | expand: true, 444 | cwd: "brackets-src/src", 445 | src: "*.html", 446 | dest: "brackets-dist" 447 | }] 448 | } 449 | }, 450 | compress: { 451 | main: { 452 | options: { 453 | mode: "gzip" 454 | }, 455 | files: [ 456 | { 457 | expand: true, 458 | cwd: "brackets-dist/", 459 | src: [ 460 | "**/*.js", 461 | "!samples/**", 462 | "!extensions/default/new-project/templateFiles/**" 463 | ], 464 | extDot: "last", 465 | dest: "brackets-dist/", 466 | ext: ".js.gz" 467 | }, 468 | { 469 | expand: true, 470 | cwd: "brackets-dist/", 471 | src: [ 472 | "**/*.css", 473 | "!samples/**", 474 | "!extensions/default/new-project/templateFiles/**" 475 | ], 476 | extDot: "last", 477 | dest: "brackets-dist/", 478 | ext: ".css.gz" 479 | } 480 | ] 481 | } 482 | } 483 | }; 484 | 485 | addDefaultExtesions(config); 486 | addEmbeddedExtesions(config); 487 | addCodeMirrorModes(config); 488 | grunt.initConfig(config); 489 | 490 | var common = require("./brackets-src/tasks/lib/common")(grunt), 491 | build = require("./brackets-src/tasks/build")(grunt); 492 | 493 | grunt.registerTask("build-config", "Update config.json with the build timestamp, branch and SHA being built", function () { 494 | var done = this.async(), 495 | distConfig = grunt.file.readJSON("brackets-src/src/config.json"); 496 | 497 | build.getGitInfo(path.resolve("./brackets-src")).then(function (gitInfo) { 498 | distConfig.version = distConfig.version.substr(0, distConfig.version.lastIndexOf("-") + 1) + gitInfo.commits; 499 | distConfig.repository.SHA = gitInfo.sha; 500 | distConfig.repository.branch = gitInfo.branch; 501 | distConfig.config.build_timestamp = new Date().toString().split("(")[0].trim(); 502 | 503 | common.writeJSON(grunt, "brackets-dist/config.json", distConfig); 504 | 505 | done(); 506 | }, function (err) { 507 | grunt.log.writeln(err); 508 | done(false); 509 | }); 510 | }); 511 | 512 | // task: build 513 | grunt.registerTask("build", [ 514 | "clean", 515 | "less", 516 | "targethtml", 517 | "replace", 518 | "useminPrepare", 519 | "htmlmin", 520 | "requirejs", 521 | "concat", 522 | "copy", 523 | "usemin", 524 | "compress", 525 | "build-config" 526 | ]); 527 | 528 | grunt.registerTask("test", function () { 529 | var arg = "all"; 530 | if (this.args && this.args.length > 0) { 531 | arg = this.args[0]; 532 | } 533 | 534 | grunt.task.run(["simplemocha:" + arg]); 535 | }); 536 | 537 | grunt.registerTask("test-debug", function () { 538 | var arg = "all"; 539 | if (this.args && this.args.length > 0) { 540 | arg = this.args[0]; 541 | } 542 | 543 | grunt.task.run(["concurrent:debug_" + arg]); 544 | }); 545 | 546 | grunt.registerTask("docs", ["jsdoc", "gh-pages"]); 547 | 548 | grunt.registerTask("publish", function() { 549 | var opts = { 550 | cwd: path.join(__dirname, "brackets-srv") 551 | }, 552 | arg = this.args && this.args.length > 0 ? this.args[0] : null, 553 | cmd = arg === "simulate" ? "npm install -g" : "npm publish", 554 | done = this.async(); 555 | 556 | glob("**/node_modules", opts, function (err, files) { 557 | var failure; 558 | 559 | if (err) { 560 | throw err; 561 | } 562 | 563 | if (files) { 564 | files.sort(function (a, b) { 565 | return b.length - a.length; 566 | }); 567 | 568 | files.forEach(function (file) { 569 | file = path.join(opts.cwd, file); 570 | fs.renameSync(file, file + "_"); 571 | console.log("file: " + file + "_"); 572 | }); 573 | 574 | failure = shell.exec(cmd).code; 575 | 576 | files.forEach(function (file) { 577 | file = path.join(opts.cwd, file); 578 | fs.renameSync(file + "_", file); 579 | console.log("file: " + file); 580 | }); 581 | 582 | if (failure) { 583 | grunt.fail.fatal(new Error("Execution failed for: " + cmd)); 584 | } 585 | done(); 586 | } 587 | }); 588 | }); 589 | }; 590 | -------------------------------------------------------------------------------- /lib/domains/ExtensionManagerDomain.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | 25 | /*jslint vars: true, plusplus: true, devel: true, node: true, nomen: true, 26 | indent: 4, maxerr: 50 */ 27 | 28 | "use strict"; 29 | 30 | var supportDir; 31 | 32 | var semver = require("semver"), 33 | path = require("path"), 34 | http = require("http"), 35 | request = require("request"), 36 | os = require("os"), 37 | fs = require("fs-extra"), 38 | temp = require("temp"), 39 | validate = require("./package-validator").validate; 40 | 41 | // Automatically clean up temp files on exit 42 | temp.track(); 43 | 44 | var Errors = { 45 | API_NOT_COMPATIBLE: "API_NOT_COMPATIBLE", 46 | MISSING_REQUIRED_OPTIONS: "MISSING_REQUIRED_OPTIONS", 47 | DOWNLOAD_ID_IN_USE: "DOWNLOAD_ID_IN_USE", 48 | BAD_HTTP_STATUS: "BAD_HTTP_STATUS", // {0} is the HTTP status code 49 | NO_SERVER_RESPONSE: "NO_SERVER_RESPONSE", 50 | CANNOT_WRITE_TEMP: "CANNOT_WRITE_TEMP", 51 | CANCELED: "CANCELED" 52 | }; 53 | 54 | var Statuses = { 55 | FAILED: "FAILED", 56 | INSTALLED: "INSTALLED", 57 | ALREADY_INSTALLED: "ALREADY_INSTALLED", 58 | SAME_VERSION: "SAME_VERSION", 59 | OLDER_VERSION: "OLDER_VERSION", 60 | NEEDS_UPDATE: "NEEDS_UPDATE", 61 | DISABLED: "DISABLED" 62 | }; 63 | 64 | /** 65 | * Maps unique download ID to info about the pending download. No entry if download no longer pending. 66 | * outStream is only present if we've started receiving the body. 67 | * @type {Object.} 68 | */ 69 | var pendingDownloads = {}; 70 | 71 | /** 72 | * Private function to remove the installation directory if the installation fails. 73 | * This does not call any callbacks. It's assumed that the callback has already been called 74 | * and this cleanup routine will do its best to complete in the background. If there's 75 | * a problem here, it is simply logged with console.error. 76 | * 77 | * @param {string} installDirectory Directory to remove 78 | */ 79 | function _removeFailedInstallation(installDirectory) { 80 | fs.remove(installDirectory, function (err) { 81 | if (err) { 82 | console.error("Error while removing directory after failed installation", installDirectory, err); 83 | } 84 | }); 85 | } 86 | 87 | /** 88 | * Private function to unzip to the correct directory. 89 | * 90 | * @param {string} Absolute path to the package zip file 91 | * @param {string} Absolute path to the destination directory for unzipping 92 | * @param {Object} the return value with the useful information for the client 93 | * @param {Function} callback function that is called at the end of the unzipping 94 | */ 95 | function _performInstall(packagePath, installDirectory, validationResult, callback) { 96 | validationResult.installedTo = installDirectory; 97 | 98 | var callbackCalled = false; 99 | 100 | fs.mkdirs(installDirectory, function (err) { 101 | if (err) { 102 | callback(err); 103 | return; 104 | } 105 | var sourceDir = path.join(validationResult.extractDir, validationResult.commonPrefix); 106 | 107 | fs.copy(sourceDir, installDirectory, function (err) { 108 | if (err) { 109 | _removeFailedInstallation(installDirectory); 110 | callback(err, null); 111 | } else { 112 | // The status may have already been set previously (as in the 113 | // DISABLED case. 114 | if (!validationResult.installationStatus) { 115 | validationResult.installationStatus = Statuses.INSTALLED; 116 | } 117 | callback(null, validationResult); 118 | } 119 | }); 120 | }); 121 | } 122 | 123 | /** 124 | * Private function to remove the target directory and then install. 125 | * 126 | * @param {string} Absolute path to the package zip file 127 | * @param {string} Absolute path to the destination directory for unzipping 128 | * @param {Object} the return value with the useful information for the client 129 | * @param {Function} callback function that is called at the end of the unzipping 130 | */ 131 | function _removeAndInstall(packagePath, installDirectory, validationResult, callback) { 132 | // If this extension was previously installed but disabled, we will overwrite the 133 | // previous installation in that directory. 134 | fs.remove(installDirectory, function (err) { 135 | if (err) { 136 | callback(err); 137 | return; 138 | } 139 | _performInstall(packagePath, installDirectory, validationResult, callback); 140 | }); 141 | } 142 | 143 | function _checkExistingInstallation(validationResult, installDirectory, systemInstallDirectory, callback) { 144 | // If the extension being installed does not have a package.json, we can't 145 | // do any kind of version comparison, so we just signal to the UI that 146 | // it already appears to be installed. 147 | if (!validationResult.metadata) { 148 | validationResult.installationStatus = Statuses.ALREADY_INSTALLED; 149 | callback(null, validationResult); 150 | return; 151 | } 152 | 153 | fs.readJson(path.join(installDirectory, "package.json"), function (err, packageObj) { 154 | // if the package.json is unreadable, we assume that the new package is an update 155 | // that is the first to include a package.json. 156 | if (err) { 157 | validationResult.installationStatus = Statuses.NEEDS_UPDATE; 158 | } else { 159 | // Check to see if the version numbers signal an update. 160 | if (semver.lt(packageObj.version, validationResult.metadata.version)) { 161 | validationResult.installationStatus = Statuses.NEEDS_UPDATE; 162 | } else if (semver.gt(packageObj.version, validationResult.metadata.version)) { 163 | // Pass a message back to the UI that the new package appears to be an older version 164 | // than what's installed. 165 | validationResult.installationStatus = Statuses.OLDER_VERSION; 166 | validationResult.installedVersion = packageObj.version; 167 | } else { 168 | // Signal to the UI that it looks like the user is re-installing the 169 | // same version. 170 | validationResult.installationStatus = Statuses.SAME_VERSION; 171 | } 172 | } 173 | callback(null, validationResult); 174 | }); 175 | } 176 | 177 | /** 178 | * A "legacy package" is an extension that was installed based on the GitHub name without 179 | * a package.json file. Checking for the presence of these legacy extensions will help 180 | * users upgrade if the extension developer puts a different name in package.json than 181 | * the name of the GitHub project. 182 | * 183 | * @param {string} legacyDirectory directory to check for old-style extension. 184 | */ 185 | function legacyPackageCheck(legacyDirectory) { 186 | return fs.existsSync(legacyDirectory) && !fs.existsSync(path.join(legacyDirectory, "package.json")); 187 | } 188 | 189 | /** 190 | * Implements the "install" command in the "extensions" domain. 191 | * 192 | * There is no need to call validate independently. Validation is the first 193 | * thing that is done here. 194 | * 195 | * After the extension is validated, it is installed in destinationDirectory 196 | * unless the extension is already present there. If it is already present, 197 | * a determination is made about whether the package being installed is 198 | * an update. If it does appear to be an update, then result.installationStatus 199 | * is set to NEEDS_UPDATE. If not, then it's set to ALREADY_INSTALLED. 200 | * 201 | * If the installation succeeds, then result.installationStatus is set to INSTALLED. 202 | * 203 | * The extension is unzipped into a directory in destinationDirectory with 204 | * the name of the extension (the name is derived either from package.json 205 | * or the name of the zip file). 206 | * 207 | * The destinationDirectory will be created if it does not exist. 208 | * 209 | * @param {string} Absolute path to the package zip file 210 | * @param {string} the destination directory 211 | * @param {{disabledDirectory: !string, apiVersion: !string, nameHint: ?string, 212 | * systemExtensionDirectory: !string}} additional settings to control the installation 213 | * @param {function} callback (err, result) 214 | * @param {boolean} _doUpdate private argument to signal that an update should be performed 215 | */ 216 | function _cmdInstall(packagePath, destinationDirectory, options, callback, _doUpdate) { 217 | if (!options || !options.disabledDirectory || !options.apiVersion || !options.systemExtensionDirectory) { 218 | callback(new Error(Errors.MISSING_REQUIRED_OPTIONS), null); 219 | return; 220 | } 221 | 222 | var validateCallback = function (err, validationResult) { 223 | validationResult.localPath = packagePath; 224 | 225 | if (destinationDirectory.indexOf("/support/") === 0) { 226 | destinationDirectory = path.join(supportDir, destinationDirectory.substr(8)); 227 | } 228 | 229 | // This is a wrapper for the callback that will delete the temporary 230 | // directory to which the package was unzipped. 231 | function deleteTempAndCallback(err) { 232 | if (validationResult.extractDir) { 233 | fs.remove(validationResult.extractDir); 234 | delete validationResult.extractDir; 235 | } 236 | callback(err, validationResult); 237 | } 238 | 239 | // If there was trouble at the validation stage, we stop right away. 240 | if (err || validationResult.errors.length > 0) { 241 | validationResult.installationStatus = Statuses.FAILED; 242 | deleteTempAndCallback(err, validationResult); 243 | return; 244 | } 245 | 246 | // Prefers the package.json name field, but will take the zip 247 | // file's name if that's all that's available. 248 | var extensionName, guessedName; 249 | if (options.nameHint) { 250 | guessedName = path.basename(options.nameHint, ".zip"); 251 | } else { 252 | guessedName = path.basename(packagePath, ".zip"); 253 | } 254 | if (validationResult.metadata) { 255 | extensionName = validationResult.metadata.name; 256 | } else { 257 | extensionName = guessedName; 258 | } 259 | 260 | validationResult.name = extensionName; 261 | var installDirectory = path.join(destinationDirectory, extensionName), 262 | legacyDirectory = path.join(destinationDirectory, guessedName), 263 | systemInstallDirectory = path.join(options.systemExtensionDirectory, extensionName); 264 | 265 | if (validationResult.metadata && validationResult.metadata.engines && 266 | validationResult.metadata.engines.brackets) { 267 | var compatible = semver.satisfies(options.apiVersion, 268 | validationResult.metadata.engines.brackets); 269 | if (!compatible) { 270 | installDirectory = path.join(options.disabledDirectory, extensionName); 271 | validationResult.installationStatus = Statuses.DISABLED; 272 | validationResult.disabledReason = Errors.API_NOT_COMPATIBLE; 273 | _removeAndInstall(packagePath, installDirectory, validationResult, deleteTempAndCallback); 274 | return; 275 | } 276 | } 277 | 278 | // The "legacy" stuff should go away after all of the commonly used extensions 279 | // have been upgraded with package.json files. 280 | var hasLegacyPackage = validationResult.metadata && legacyPackageCheck(legacyDirectory); 281 | 282 | // If the extension is already there, we signal to the front end that it's already installed 283 | // unless the front end has signaled an intent to update. 284 | if (hasLegacyPackage || fs.existsSync(installDirectory) || fs.existsSync(systemInstallDirectory)) { 285 | if (_doUpdate) { 286 | if (hasLegacyPackage) { 287 | // When there's a legacy installed extension, remove it first, 288 | // then also remove any new-style directory the user may have. 289 | // This helps clean up if the user is in a state where they have 290 | // both legacy and new extensions installed. 291 | fs.remove(legacyDirectory, function (err) { 292 | if (err) { 293 | deleteTempAndCallback(err, validationResult); 294 | return; 295 | } 296 | _removeAndInstall(packagePath, installDirectory, validationResult, deleteTempAndCallback); 297 | }); 298 | } else { 299 | _removeAndInstall(packagePath, installDirectory, validationResult, deleteTempAndCallback); 300 | } 301 | } else if (hasLegacyPackage) { 302 | validationResult.installationStatus = Statuses.NEEDS_UPDATE; 303 | validationResult.name = guessedName; 304 | deleteTempAndCallback(null, validationResult); 305 | } else { 306 | _checkExistingInstallation(validationResult, installDirectory, systemInstallDirectory, deleteTempAndCallback); 307 | } 308 | } else { 309 | // Regular installation with no conflicts. 310 | validationResult.disabledReason = null; 311 | _performInstall(packagePath, installDirectory, validationResult, deleteTempAndCallback); 312 | } 313 | }; 314 | 315 | validate(packagePath, {}, validateCallback); 316 | } 317 | 318 | /** 319 | * Implements the "update" command in the "extensions" domain. 320 | * 321 | * Currently, this just wraps _cmdInstall, but will remove the existing directory 322 | * first. 323 | * 324 | * There is no need to call validate independently. Validation is the first 325 | * thing that is done here. 326 | * 327 | * After the extension is validated, it is installed in destinationDirectory 328 | * unless the extension is already present there. If it is already present, 329 | * a determination is made about whether the package being installed is 330 | * an update. If it does appear to be an update, then result.installationStatus 331 | * is set to NEEDS_UPDATE. If not, then it's set to ALREADY_INSTALLED. 332 | * 333 | * If the installation succeeds, then result.installationStatus is set to INSTALLED. 334 | * 335 | * The extension is unzipped into a directory in destinationDirectory with 336 | * the name of the extension (the name is derived either from package.json 337 | * or the name of the zip file). 338 | * 339 | * The destinationDirectory will be created if it does not exist. 340 | * 341 | * @param {string} Absolute path to the package zip file 342 | * @param {string} the destination directory 343 | * @param {{disabledDirectory: !string, apiVersion: !string, nameHint: ?string, 344 | * systemExtensionDirectory: !string}} additional settings to control the installation 345 | * @param {function} callback (err, result) 346 | */ 347 | function _cmdUpdate(packagePath, destinationDirectory, options, callback) { 348 | _cmdInstall(packagePath, destinationDirectory, options, callback, true); 349 | } 350 | 351 | /** 352 | * Wrap up after the given download has terminated (successfully or not). Closes connections, calls back the 353 | * client's callback, and IF there was an error, delete any partially-downloaded file. 354 | * 355 | * @param {string} downloadId Unique id originally passed to _cmdDownloadFile() 356 | * @param {?string} error If null, download was treated as successful 357 | */ 358 | function _endDownload(downloadId, error) { 359 | var downloadInfo = pendingDownloads[downloadId]; 360 | delete pendingDownloads[downloadId]; 361 | 362 | if (error) { 363 | // Abort the download if still pending 364 | // Note that this will trigger response's "end" event 365 | downloadInfo.request.abort(); 366 | 367 | // Clean up any partially-downloaded file 368 | // (if no outStream, then we never got a response back yet and never created any file) 369 | if (downloadInfo.outStream) { 370 | downloadInfo.outStream.end(function () { 371 | fs.unlink(downloadInfo.localPath); 372 | }); 373 | } 374 | 375 | downloadInfo.callback(error, null); 376 | 377 | } else { 378 | // Download completed successfully. Flush stream to disk and THEN signal completion 379 | downloadInfo.outStream.end(function () { 380 | downloadInfo.callback(null, downloadInfo.localPath); 381 | }); 382 | } 383 | } 384 | 385 | /** 386 | * Implements "downloadFile" command, asynchronously. 387 | */ 388 | function _cmdDownloadFile(downloadId, url, proxy, callback) { 389 | // Backwards compatibility check, added in 0.37 390 | if (typeof proxy === "function") { 391 | callback = proxy; 392 | proxy = undefined; 393 | } 394 | 395 | if (pendingDownloads[downloadId]) { 396 | callback(Errors.DOWNLOAD_ID_IN_USE, null); 397 | return; 398 | } 399 | 400 | var https = require("https"); 401 | 402 | var req = https.get(url, function(res) { 403 | if (res.statusCode !== 200) { 404 | _endDownload(downloadId, [Errors.BAD_HTTP_STATUS, res.statusCode]); 405 | return; 406 | } 407 | 408 | var stream = temp.createWriteStream("brackets"); 409 | if (!stream) { 410 | _endDownload(downloadId, Errors.CANNOT_WRITE_TEMP); 411 | return; 412 | } 413 | pendingDownloads[downloadId].localPath = stream.path; 414 | pendingDownloads[downloadId].outStream = stream; 415 | 416 | res.on("data", function(d) { 417 | stream.write(d); 418 | }); 419 | 420 | res.on("end", function () { 421 | _endDownload(downloadId); 422 | }); 423 | 424 | }).on("error", function(e) { 425 | console.error(e); 426 | _endDownload(downloadId, e.message); 427 | }); 428 | 429 | pendingDownloads[downloadId] = { request: req, callback: callback }; 430 | } 431 | 432 | /** 433 | * Implements "abortDownload" command, synchronously. 434 | */ 435 | function _cmdAbortDownload(downloadId) { 436 | if (!pendingDownloads[downloadId]) { 437 | // This may mean the download already completed 438 | return false; 439 | } else { 440 | _endDownload(downloadId, Errors.CANCELED); 441 | return true; 442 | } 443 | } 444 | 445 | /** 446 | * Implements the remove extension command. 447 | */ 448 | function _cmdRemove(extensionDir, callback) { 449 | if (extensionDir.indexOf("/support/") === 0) { 450 | extensionDir = path.join(supportDir, extensionDir.substr(8)); 451 | } 452 | 453 | fs.remove(extensionDir, function (err) { 454 | if (err) { 455 | callback(err); 456 | } else { 457 | callback(null); 458 | } 459 | }); 460 | } 461 | 462 | /** 463 | * Initialize the "extensions" domain. 464 | * The extensions domain handles downloading, unpacking/verifying, and installing extensions. 465 | */ 466 | function init(domainManager) { 467 | supportDir = domainManager.supportDir; 468 | if (!domainManager.hasDomain("extensionManager")) { 469 | domainManager.registerDomain("extensionManager", {major: 0, minor: 1}); 470 | } 471 | domainManager.registerCommand( 472 | "extensionManager", 473 | "validate", 474 | validate, 475 | true, 476 | "Verifies that the contents of the given ZIP file are a valid Brackets extension package", 477 | [{ 478 | name: "path", 479 | type: "string", 480 | description: "absolute filesystem path of the extension package" 481 | }, { 482 | name: "options", 483 | type: "{requirePackageJSON: ?boolean}", 484 | description: "options to control the behavior of the validator" 485 | }], 486 | [{ 487 | name: "errors", 488 | type: "string|Array.", 489 | description: "download error, if any; first string is error code (one of Errors.*); subsequent strings are additional info" 490 | }, { 491 | name: "metadata", 492 | type: "{name: string, version: string}", 493 | description: "all package.json metadata (null if there's no package.json)" 494 | }] 495 | ); 496 | domainManager.registerCommand( 497 | "extensionManager", 498 | "install", 499 | _cmdInstall, 500 | true, 501 | "Installs the given Brackets extension if it is valid (runs validation command automatically)", 502 | [{ 503 | name: "path", 504 | type: "string", 505 | description: "absolute filesystem path of the extension package" 506 | }, { 507 | name: "destinationDirectory", 508 | type: "string", 509 | description: "absolute filesystem path where this extension should be installed" 510 | }, { 511 | name: "options", 512 | type: "{disabledDirectory: !string, apiVersion: !string, nameHint: ?string, systemExtensionDirectory: !string}", 513 | description: "installation options: disabledDirectory should be set so that extensions can be installed disabled." 514 | }], 515 | [{ 516 | name: "errors", 517 | type: "string|Array.", 518 | description: "download error, if any; first string is error code (one of Errors.*); subsequent strings are additional info" 519 | }, { 520 | name: "metadata", 521 | type: "{name: string, version: string}", 522 | description: "all package.json metadata (null if there's no package.json)" 523 | }, { 524 | name: "disabledReason", 525 | type: "string", 526 | description: "reason this extension was installed disabled (one of Errors.*), none if it was enabled" 527 | }, { 528 | name: "installationStatus", 529 | type: "string", 530 | description: "Current status of the installation (an extension can be valid but not installed because it's an update" 531 | }, { 532 | name: "installedTo", 533 | type: "string", 534 | description: "absolute path where the extension was installed to" 535 | }, { 536 | name: "commonPrefix", 537 | type: "string", 538 | description: "top level directory in the package zip which contains all of the files" 539 | }] 540 | ); 541 | domainManager.registerCommand( 542 | "extensionManager", 543 | "update", 544 | _cmdUpdate, 545 | true, 546 | "Updates the given Brackets extension (for which install was generally previously attemped). Brackets must be quit after this.", 547 | [{ 548 | name: "path", 549 | type: "string", 550 | description: "absolute filesystem path of the extension package" 551 | }, { 552 | name: "destinationDirectory", 553 | type: "string", 554 | description: "absolute filesystem path where this extension should be installed" 555 | }, { 556 | name: "options", 557 | type: "{disabledDirectory: !string, apiVersion: !string, nameHint: ?string, systemExtensionDirectory: !string}", 558 | description: "installation options: disabledDirectory should be set so that extensions can be installed disabled." 559 | }], 560 | [{ 561 | name: "errors", 562 | type: "string|Array.", 563 | description: "download error, if any; first string is error code (one of Errors.*); subsequent strings are additional info" 564 | }, { 565 | name: "metadata", 566 | type: "{name: string, version: string}", 567 | description: "all package.json metadata (null if there's no package.json)" 568 | }, { 569 | name: "disabledReason", 570 | type: "string", 571 | description: "reason this extension was installed disabled (one of Errors.*), none if it was enabled" 572 | }, { 573 | name: "installationStatus", 574 | type: "string", 575 | description: "Current status of the installation (an extension can be valid but not installed because it's an update" 576 | }, { 577 | name: "installedTo", 578 | type: "string", 579 | description: "absolute path where the extension was installed to" 580 | }, { 581 | name: "commonPrefix", 582 | type: "string", 583 | description: "top level directory in the package zip which contains all of the files" 584 | }] 585 | ); 586 | domainManager.registerCommand( 587 | "extensionManager", 588 | "remove", 589 | _cmdRemove, 590 | true, 591 | "Removes the Brackets extension at the given path.", 592 | [{ 593 | name: "path", 594 | type: "string", 595 | description: "absolute filesystem path of the installed extension folder" 596 | }], 597 | {} 598 | ); 599 | domainManager.registerCommand( 600 | "extensionManager", 601 | "downloadFile", 602 | _cmdDownloadFile, 603 | true, 604 | "Downloads the file at the given URL, saving it to a temp location. Callback receives path to the downloaded file.", 605 | [{ 606 | name: "downloadId", 607 | type: "string", 608 | description: "Unique identifier for this download 'session'" 609 | }, { 610 | name: "url", 611 | type: "string", 612 | description: "URL to download from" 613 | }, { 614 | name: "proxy", 615 | type: "string", 616 | description: "optional proxy URL" 617 | }], 618 | { 619 | type: "string", 620 | description: "Local path to the downloaded file" 621 | } 622 | ); 623 | domainManager.registerCommand( 624 | "extensionManager", 625 | "abortDownload", 626 | _cmdAbortDownload, 627 | false, 628 | "Aborts any pending download with the given id. Ignored if no download pending (may be already complete).", 629 | [{ 630 | name: "downloadId", 631 | type: "string", 632 | description: "Unique identifier for this download 'session', previously pased to downloadFile" 633 | }], 634 | { 635 | type: "boolean", 636 | description: "True if the download was pending and able to be canceled; false otherwise" 637 | } 638 | ); 639 | } 640 | 641 | // used in unit tests 642 | exports._cmdValidate = validate; 643 | exports._cmdInstall = _cmdInstall; 644 | exports._cmdRemove = _cmdRemove; 645 | exports._cmdUpdate = _cmdUpdate; 646 | 647 | // used to load the domain 648 | exports.init = init; 649 | --------------------------------------------------------------------------------