diff --git a/README.md b/README.md index afd8d27..2f27f3f 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,67 @@ # crx [![Build Status](https://secure.travis-ci.org/oncletom/crx.svg)](http://travis-ci.org/oncletom/crx) -crx is a [pure node.js](http://nodejs.org/) command line app for packing Google Chrome extensions. **No OpenSSL required**! +> crx is a utility to **package Google Chrome extensions** via a *Node API* and the *command line*. It is written **purely in JavaScript** and **does not require OpenSSL**! -If you'd like to integrate it into your [grunt](http://gruntjs.com/) workflow, give [grunt-crx](https://github.com/oncletom/grunt-crx) a spin. +Packages are available to use `crx` with: -Massive hat tip to the [node-rsa project](https://github.com/rzcoder/node-rsa)! +- *grunt*: [grunt-crx](https://npmjs.com/grunt-crx) +- *gulp*: [gulp-crx-pack](https://npmjs.com/gulp-crx-pack) +- *webpack*: [crx-webpack-plugin](https://npmjs.com/crx-webpack-plugin) + +Massive hat tip to the [node-rsa project](https://npmjs.com/node-rsa) for the pure JavaScript encryption! ## Install - $ npm install crx +```bash +$ npm install crx +``` ## Module API Asynchronous functions returns an [ES6 Promise](https://github.com/jakearchibald/es6-promise). +```js +const fs = require("fs"); +const ChromeExtension = require("crx"); +const crx = new ChromeExtension( + codebase: "http://localhost:8000/myFirstExtension.crx", + privateKey: fs.readFileSync("./key.pem")) +}); + +crx.load("./myFirstExtension")) + .then(crx => crx.pack()) + .then(crxBuffer => { + const updateXML = crx.generateUpdateXML() + + fs.writeFile("../update.xml"), updateXML); + fs.writeFile("../myFirstExtension.crx"), crxBuffer); + }); +``` + ### ChromeExtension = require("crx") ### crx = new ChromeExtension(attrs) This module exports the `ChromeExtension` constructor directly, which can take an optional attribute object, which is used to extend the instance. -### crx.load([path]) +### crx.load(path) -Asynchronously prepares the temporary workspace for the Chrome Extension located at `attr.rootDirectory`. +Prepares the temporary workspace for the Chrome Extension located at `path`. ```js -crx.load().then(function(crx){ +crx.load('/path/to/extension').then(crx => { // ... }); ``` -You can optionally pass a `path` argument in lieu of the `rootDirectory` constructor option. - -### crx.pack([archiveBuffer]) +### crx.pack() Packs the Chrome Extension and resolves the promise with a Buffer containing the `.crx` file. ```js -crx.pack().then(function(crxBuffer){ - fs.writeFile('/tmp/foobar.crx', crxBuffer); -}); -``` - -You can optionally pass an `archiveBuffer` argument if you want a finer grained control over the packing process: - -```js -crx.load() - .then(function(){ - return crx.loadContents(); - }) - .then(function(archiveBuffer){ - fs.writeFile('path/to/extension.zip', archiveBuffer); - - return crx.pack(archiveBuffer); - }) - .then(function(crxBuffer){ - fs.writeFile('path/to/extension.crx', crxBuffer); +crx.load('/path/to/extension') + .then(crx => crx.pack()) + .then(crxBuffer => { + fs.writeFile('/tmp/foobar.crx', crxBuffer); }); ``` @@ -63,35 +70,14 @@ crx.load() Returns a Buffer containing the update.xml file used for `autoupdate`, as specified for `update_url` in the manifest. In this case, the instance must have a property called `codebase`. ```js -var crx = new ChromeExtension({ ..., codebase: 'https://autoupdateserver.com/myFirstExtension.crx' }); - -crx.pack().then(function(crxBuffer){ - // ... - - var xmlBuffer = crx.generateUpdateXML(); - fs.writeFile('/foo/bar/update.xml', xmlBuffer); -}); -``` - -## Module example - -```javascript -var fs = require("fs"), -var ChromeExtension = require("crx"), -var join = require("path").join, -var crx = new ChromeExtension( - codebase: "http://localhost:8000/myFirstExtension.crx", - privateKey: fs.readFileSync(join(__dirname, "key.pem")) -}); - -crx.load(join(__dirname, "myFirstExtension")) - .then(function() { - return crx.pack().then(function(crxBuffer){ - var updateXML = crx.generateUpdateXML() - - fs.writeFile(join(__dirname, "update.xml"), updateXML) - fs.writeFile(join(__dirname, "myFirstExtension.crx"), crxBuffer) - }) +const crx = new ChromeExtension({ ..., codebase: 'https://autoupdateserver.com/myFirstExtension.crx' }); + +crx.load('/path/to/extension') + .then(crx => crx.pack()) + .then(crxBuffer => { + // ... + const xmlBuffer = crx.generateUpdateXML(); + fs.writeFile('/foo/bar/update.xml', xmlBuffer); }); ``` @@ -123,50 +109,64 @@ Show information about using this utility, generated by [commander](https://gith Given the following directory structure: - └─┬ myFirstExtension - ├── manifest.json - └── icon.png +``` +└─┬ myFirstExtension + ├── manifest.json + └── icon.png +``` run this: - cd myFirstExtension - crx pack -o +```bash +$ cd myFirstExtension +$ crx pack -o +``` to generate this: - ├─┬ myFirstExtension - │ ├── manifest.json - │ ├── icon.png - │ └── key.pem - └── myFirstExtension.crx +```bash +├─┬ myFirstExtension +│ ├── manifest.json +│ ├── icon.png +│ └── key.pem +└── myFirstExtension.crx +``` You can also name the output file like this: - cd myFirstExtension - crx pack -o myFirstExtension.crx +```bash +$ cd myFirstExtension +$ crx pack -o myFirstExtension.crx +``` to get the same results, or also pipe to the file manually like this. - cd myFirstExtension - crx pack > ../myFirstExtension.crx +```bash +$ cd myFirstExtension +$ crx pack > ../myFirstExtension.crx +``` As you can see a key is generated for you at `key.pem` if none exists. You can also specify an external key. So if you have this: - ├─┬ myFirstExtension - │ ├── manifest.json - │ └── icon.png - └── myPrivateKey.pem +``` +├─┬ myFirstExtension +│ ├── manifest.json +│ └── icon.png +└── myPrivateKey.pem +``` you can run this: - crx pack myFirstExtension -p myPrivateKey.pem -o +```bash +$ crx pack myFirstExtension -p myPrivateKey.pem -o +``` to sign your package without keeping the key in the directory. Copyright --------- - Copyright (c) 2014 Jed Schmidt, Thomas Parisot + Copyright (c) 2016 Jed Schmidt, Thomas Parisot and collaborators Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -186,4 +186,3 @@ Copyright 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. - diff --git a/package.json b/package.json index bba6e9b..3fd0af9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "author": "Jed Schmidt (http://jed.is)", "name": "crx", - "description": "Build Google Chrome extensions with node.js", + "description": "crx is a utility to package Google Chrome extensions via a Node API and the command line", "version": "3.0.4", "license": "MIT", "homepage": "https://github.com/oncletom/crx", @@ -19,31 +19,29 @@ }, "scripts": { "test": "nyc tape ./test/*.js", - "posttest": "nyc report --reporter=html", "version": "npm run changelog && git add CHANGELOG.md", "changelog": "github-changes -o oncletom -r crx -n ${npm_package_version} --only-pulls --use-commit-body" }, "nyc": { "functions": 100, - "statements": 95, - "branches": 88, + "statements": 100, + "branches": 100, "check-coverage": true, "reporter": [ - "text" + "text", + "html" ] }, "dependencies": { "archiver": "^1.1.0", "commander": "^2.5.0", "es6-promise": "^3.0.0", - "node-rsa": "^0.2.10", - "temp": "^0.8.1", - "wrench": "^1.5.0" + "node-rsa": "^0.2.10" }, "devDependencies": { + "adm-zip": "^0.4.7", "github-changes": "^1.0.0", "nyc": "^8.3.0", - "sinon": "^1.12.1", - "tape": "^3.0.3" + "tape": "^4.6.0" } } diff --git a/src/crx.js b/src/crx.js index 30a35c0..372359a 100644 --- a/src/crx.js +++ b/src/crx.js @@ -5,11 +5,9 @@ var fs = require("fs"); var path = require("path"); var join = path.join; var crypto = require("crypto"); -var RSA = require('node-rsa'); -var wrench = require("wrench"); +var RSA = require("node-rsa"); var archiver = require("archiver"); -var Promise = require('es6-promise').Promise; -var temp = require('temp'); +var Promise = require("es6-promise").Promise; function ChromeExtension(attrs) { if ((this instanceof ChromeExtension) !== true) { @@ -21,10 +19,6 @@ function ChromeExtension(attrs) { */ this.appId = null; - this.manifest = ''; - - this.loaded = false; - this.rootDirectory = ''; this.publicKey = null; @@ -33,6 +27,8 @@ function ChromeExtension(attrs) { this.codebase = null; + this.path = null; + /* Copying attributes */ @@ -40,8 +36,7 @@ function ChromeExtension(attrs) { this[name] = attrs[name]; } - temp.track(); - this.path = temp.mkdirSync('crx'); + this.loaded = false; } ChromeExtension.prototype = { @@ -66,8 +61,7 @@ ChromeExtension.prototype = { var selfie = this; var packP = [ this.generatePublicKey(), - contentsBuffer || selfie.loadContents(), - this.writeFile("manifest.json", JSON.stringify(selfie.manifest)) + contentsBuffer || selfie.loadContents() ]; return Promise.all(packP).then(function(outputs){ @@ -92,22 +86,19 @@ ChromeExtension.prototype = { var selfie = this; return new Promise(function(resolve, reject){ - wrench.copyDirRecursive(path || selfie.rootDirectory, selfie.path, {forceDelete: true}, function (err) { - if (err) { - return reject(err); - } + selfie.path = path || selfie.rootDirectory; - selfie.manifest = require(join(selfie.path, "manifest.json")); - selfie.loaded = true; + selfie.manifest = require(join(selfie.path, "manifest.json")); + selfie.loaded = true; - resolve(selfie); - }); + resolve(selfie); }); }, /** * Writes data into the extension workable directory. * + * @deprecated * @param {string} path * @param {*} data * @returns {Promise} @@ -115,6 +106,7 @@ ChromeExtension.prototype = { writeFile: function (path, data) { var absPath = join(this.path, path); + /* istanbul ignore next */ return new Promise(function(resolve, reject){ fs.writeFile(absPath, data, function (err) { if (err) { @@ -174,60 +166,40 @@ ChromeExtension.prototype = { * @returns {Promise} */ loadContents: function () { - var archive = archiver("zip"); var selfie = this; return new Promise(function(resolve, reject){ + var archive = archiver('zip'); var contents = new Buffer(''); - var allFiles = []; if (!selfie.loaded) { throw new Error('crx.load needs to be called first in order to prepare the workspace.'); } - // the callback is called many times - // when 'files' is null, it means we accumulated everything - // hence this weird setup - wrench.readdirRecursive(selfie.path, function(err, files){ - if (err){ - return reject(err); - } - - // stack unless 'files' is null - if (files){ - allFiles = allFiles.concat(files); - return; - } - - allFiles.forEach(function (file) { - var filePath = join(selfie.path, file); - var stat = fs.statSync(filePath); - - if (stat.isFile() && file !== "key.pem") { - archive.append(fs.createReadStream(filePath), { name: file }); - } - }); + archive.on('error', reject); - archive.finalize(); + /* + TODO: Remove in v4. + It will be better to resolve an archive object + rather than fitting everything in memory. - // Relates to the issue: "Event 'finished' no longer valid #18" - // https://github.com/jed/crx/issues/18 - // TODO: Buffer concat could be a problem when building a big extension. - // So ideally only the 'finish' callback must be used. - archive.on('readable', function () { - var buf = archive.read(); - - if (buf) { - contents = Buffer.concat([contents, buf]); - } - }); - - archive.on('finish', function () { - resolve(contents); - }); + @see https://github.com/oncletom/crx/issues/61 + */ + archive.on('data', function (buf) { + contents = Buffer.concat([contents, buf]); + }); - archive.on("error", reject); + archive.on('finish', function () { + resolve(contents); }); + + archive + .glob('**', { + cwd: selfie.path, + matchBase: true, + ignore: ['*.pem', '.git', '*.crx'] + }) + .finalize(); }); }, diff --git a/test/index.js b/test/index.js index 393a832..2262aff 100644 --- a/test/index.js +++ b/test/index.js @@ -3,16 +3,16 @@ var fs = require("fs"); var test = require("tape"); +var Zip = require("adm-zip"); var ChromeExtension = require("../"); var join = require("path").join; var privateKey = fs.readFileSync(join(__dirname, "key.pem")); var updateXml = fs.readFileSync(join(__dirname, "expectations", "update.xml")); -var sinon = require('sinon'); -var sandbox = sinon.sandbox.create(); function newCrx(){ return new ChromeExtension({ privateKey: privateKey, + path: '/tmp', codebase: "http://localhost:8000/myFirstExtension.crx", rootDirectory: join(__dirname, "myFirstExtension") }); @@ -45,21 +45,42 @@ test('#pack', function(t){ .catch(t.error.bind(t)); }); +test('#writeFile', function(t){ + t.plan(1); + + var crx = newCrx(); + + crx.writeFile('/tmp/crx', new Error('')).catch(function(err){ + t.ok(err); + }); +}); + test('#loadContents', function(t){ t.plan(2); var crx = newCrx(); - var loadContentsSpy = sandbox.spy(crx, 'loadContents'); crx.load().then(function(){ return crx.loadContents(); }) .then(function(contentsBuffer){ t.ok(contentsBuffer instanceof Buffer); - t.ok(loadContentsSpy.callCount === 1); + + return contentsBuffer; }) - .then(function() { - sandbox.restore(); + .then(function(packageData){ + var entries = new Zip(packageData) + .getEntries() + .map(function(entry){ + return entry.entryName; + }) + .sort(function(a, b){ + return a.localeCompare(b); + }); + + t.deepEqual(entries, ['icon.png', 'manifest.json']); + + return packageData; }) .catch(t.error.bind(t)); }); diff --git a/test/myFirstExtension/key.pem b/test/myFirstExtension/key.pem new file mode 100644 index 0000000..00713c9 --- /dev/null +++ b/test/myFirstExtension/key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQCx1nHfHr1yCt21hPRWE9yKwC8Xuq+y/dPDrkjJ8cVsgPRoq25i +iej2siZEWryDR7WWuAmdGaUtBFgQyRvCyGQS4YtkKot8iOTFzJo656hgHJUvZP2Q +Yy7ERJ3rZRwLxpWmvYQiXx92LSy19eC6Bi5+FAaTMCQNDdklanijb5D6fwIDAQAB +AoGAUQmPSkUPvvAEp7q2PKNAVFnPG9kOR1ozLXA16xApDpCUzz2PR4fgiMoVdgCC +9q+up8elWdld022vU7bQ16nJL64GFzgCHubRBM7d5UJjSaVkcDL6QbzAEIwwa1IU +LQA7BheshHs3d+gYRjedNGR0kKu3HeUofhhXLC1WXPCOZYECQQDrKqFPro6jRYjD +T65qvoIetRKw1L8J9iw6UhWb1jMtCgvTKS99zmnJzM2hOsCAy+HDj/YwlCWEKIjh +ZICZBFkpAkEAwZellq94wisJd4ptfQvHSWVPdMXR59jQcW23dBqU4D+itc1wCVT/ +xg5iIBaujTHgaM9oS0kawhWE1mmQbyyjZwJAHfVvWXRWbYxlMOSMxsKAVyMgP3DK +6Zz343IjmJfAK0O1X/BGQZOzPGcf5yNR9NaEa2KCrYuh/+UeEwC3tUatiQJANM9/ +lomrsZw36upST+hkpvsCH+LPDiYxRqAdiYiu0DXL1ziBtaoAVDEcR5CocVAH3c+m +rdL1f7iLEkqd4hYVRQJAP+uM18zCINdxYeZn+HVF3p5JOJii658+x08+0L69+DtV +FFfRJfW98rCa5F2mQivCkwpChFq4NWSqpLT3x5WuXg== +-----END RSA PRIVATE KEY-----