├── .gitignore ├── .npmignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── assets └── appc-npm ├── bin └── appc-npm ├── index.js ├── lib └── types │ ├── block.js │ ├── connector.js │ ├── lib.js │ ├── module.js │ ├── sync.js │ ├── theme.js │ └── widget.js ├── package.json └── test ├── block-test.js ├── connector-test.js ├── fixtures ├── block │ ├── README.md │ └── geocode.js ├── connector │ ├── appc.json │ ├── index.js │ └── package.json ├── lib │ ├── README.md │ └── helper.js ├── module-multiple │ ├── android │ │ ├── dist │ │ │ └── com.foo-android-1.0.2.zip │ │ └── manifest │ └── ios │ │ ├── dist │ │ └── com.foo-iphone-1.0.1.zip │ │ └── manifest ├── module-single │ ├── dist │ │ └── com.foo-iphone-1.0.1.zip │ └── manifest ├── sync │ ├── README.md │ └── restapi.js ├── theme │ └── config.json └── widget │ ├── README.md │ ├── controllers │ └── widget.js │ ├── styles │ └── widget.tss │ ├── views │ └── widget.xml │ └── widget.json ├── lib-test.js ├── module-test.js ├── sync-test.js ├── theme-test.js └── widget-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | npm-debug.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | test -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | var test = grunt.option('test') || '*'; 3 | 4 | // Project configuration. 5 | grunt.initConfig({ 6 | mochaTest: { 7 | options: { 8 | timeout: 30000, 9 | reporter: 'spec', 10 | ignoreLeaks: false 11 | }, 12 | src: ['test/' + test + '-test.js'] 13 | }, 14 | jshint: { 15 | options: { 16 | jshintrc: true 17 | }, 18 | src: ['*.js', 'assets/appc-npm', 'bin/appc-npm', 'lib/**/*.js'] 19 | }, 20 | kahvesi: { 21 | src: ['test/' + test + '-test.js'] 22 | }, 23 | clean: ['tmp'] 24 | }); 25 | 26 | // Load grunt plugins for modules 27 | grunt.loadNpmTasks('grunt-mocha-test'); 28 | grunt.loadNpmTasks('grunt-contrib-jshint'); 29 | grunt.loadNpmTasks('grunt-contrib-clean'); 30 | grunt.loadNpmTasks('grunt-kahvesi'); 31 | 32 | // register tasks 33 | grunt.registerTask('lint', 'jshint'); 34 | grunt.registerTask('test', ['lint', 'clean', 'mochaTest', 'clean']); 35 | grunt.registerTask('cover', ['kahvesi', 'clean']); 36 | 37 | grunt.registerTask('default', 'test'); 38 | }; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 by Appcelerator, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | 16 | This source code contains patents and patents pending by Appcelerator, Inc. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Appcelerator Packager for NPM 2 | Package many [types of components](#types-of-components) for Appcelerator Titanium, Alloy and Arrow projects for distribution via NPM, including [modules](#module) and [support for nested dependencies](#support-for-nested-dependencies). 3 | 4 | * [Browse Appcelerator Components on NPM](https://www.npmjs.com/browse/keyword/appc-npm) 5 | 6 | > **NOTE:** The packager only updates/adds a `package.json` and `appc-npm` postinstall executable. It adds **no dependencies** and does not change your code. 7 | 8 | ## Install the packager [![NPM](https://img.shields.io/npm/v/appc-npm.svg?style=flat-square)](https://npmjs.com/appc-npm) 9 | 10 | ``` 11 | $ [sudo] npm install -g appc-npm 12 | ``` 13 | 14 | ## Package & Publish to NPM 15 | Simply navigate to your Titanium module or library, Alloy widget, sync adapter, Arrow connector or other component and run `appc-npm [src]` with the [component type](#types-of-components) and optional path (defaulting to CWD). 16 | 17 | ``` 18 | $ cd myWidget 19 | 20 | $ appc-npm widget 21 | + alloy-widget-myWidget@1.0.0 22 | 23 | $ npm publish 24 | + alloy-widget-myWidget@1.0.0 25 | ``` 26 | 27 | You probably want to check `package.json` before you publish and set [fields](https://docs.npmjs.com/files/package.json) like `description`, `homepage`, `bugs`, `license` and `repository`. 28 | 29 | ## Install a package from NPM 30 | 31 | ``` 32 | $ npm install alloy-widget-myWidget --save 33 | 34 | > alloy-widget-myWidget@1.0.0 postinstall /Users/fokkezb/myProject/node_modules/alloy-widget-myWidget 35 | > node ./appc-npm 36 | 37 | alloy-widget-myWidget@1.0.0 node_modules/alloy-widget-myWidget 38 | ``` 39 | 40 | After which you'll find the widget in: 41 | 42 | ``` 43 | ./app/widgets/myWidget 44 | ``` 45 | 46 | For modules and widgets the bundled installer will also update the `tiapp.xml` and `app/config.json` to add the dependency. 47 | 48 | ## Support for nested dependencies 49 | You can add dependencies to other Appcelerator dependencies on NPM to the `package.json` of your packaged component. So if your Alloy widget depends on a library, module or other widget then you can install them all in one go. 50 | 51 | ``` 52 | $ npm install alloy-widget-myWidget --save 53 | 54 | > alloy-widget-myWidget@1.0.0 postinstall /Users/fokkezb/myProject/node_modules/alloy-widget-myWidget 55 | > node ./appc-npm 56 | 57 | > appc-lib-xp.ui@1.0.0 postinstall /Users/fokkezb/myProject/node_modules/alloy-widget-myWidget/node_modules/appc-lib-xp.ui 58 | > node ./appc-npm 59 | 60 | alloy-widget-myWidget@1.0.0 node_modules/alloy-widget-myWidget 61 | ├── appc-lib-xp.ui@1.0.0 62 | ``` 63 | 64 | After which you'll find the widget and the lib it depends on in: 65 | 66 | ``` 67 | ./app/widgets/myWidget 68 | ./app/lib/xp.ui.js 69 | ``` 70 | 71 | ## Update a package 72 | Run the command again to update the packaged installer and update the `package.json`'s version (for modules and widgets) and list of files to ignore or unzip by the installer. It will not overwrite any other changes you made to the `package.json`. 73 | 74 | ``` 75 | $ appc-npm widget 76 | + alloy-widget-myWidget@1.0.1 77 | ``` 78 | 79 | ## Types of Components 80 | You can package the following types of components: 81 | 82 | ### `module` 83 | Titanium modules. Run it in the [folder above the platform folders](https://github.com/viezel/NappDrawer) to package the most recent distribution ZIP file of each platform. Run it in a platform folder to package only that one. 84 | 85 | Reads the `manifest` to populate the `package.json`. It will check if the `moduleid` is available on NPM and fall back to `ti-module-` as name. It wil sum the versions of all platforms to be the package version. 86 | 87 | Only the most recent ZIP file of each platform and the `appc-npm` installer are added to the `package.json`'s `files` property so that only these will be packaged and published to NPM and not the full module source. 88 | 89 | * [Browse Titanium modules on NPM](https://www.npmjs.com/browse/keyword/ti-module) 90 | 91 | ### `lib` 92 | Titanium, Alloy or Arrow CommonJS libraries. Searches for the first `.js` and uses `alloy-sync-` as the package name and `1.0.0` for the version. All other files are ignored for the installer. 93 | 94 | * [Browse Appcelerator libraries on NPM](https://www.npmjs.com/browse/keyword/appc-lib) 95 | 96 | ### `widget` 97 | Alloy Widgets. Uses `widget.json` to populate the `package.json` and ignores that same file for the installer. It will check if the widget `id` is available on NPM and fall back to `alloy-widget-` as the package name. 98 | 99 | * [Browse Alloy widgets on NPM](https://www.npmjs.com/browse/keyword/alloy-widget) 100 | 101 | ### `sync` 102 | Alloy sync adapters. Searches for the first `.js` and uses `alloy-sync-` as the package name and `1.0.0` for the version. All other files are ignored for the installer. 103 | 104 | * [Browse Alloy sync adapters on NPM](https://www.npmjs.com/browse/keyword/alloy-sync) 105 | 106 | ### `theme` 107 | Alloy themes. Uses `alloy-sync-` as the package name and `1.0.0` for the version. It ignores the generated `package.json` for the installer. 108 | 109 | * [Browse Alloy themes adapters on NPM](https://www.npmjs.com/browse/keyword/alloy-theme) 110 | 111 | ### `connector` 112 | Arrow connectors. Searches for `package.json` to determine the target for the installer and will update the file with the `postinstall` script and `appc-npm` property, leaving the name and version as it is. 113 | 114 | * [Browse Arrow connectors on NPM](https://www.npmjs.com/browse/keyword/arrow-connector) 115 | 116 | ### `block` 117 | Arrow post or pre-blocks. Searches for the first `.js` to determine the base path and adds that file to the list of paths to copy to the project. The default package name is `arrow-block-` and version is `1.0.0`. 118 | 119 | * [Browse Arrow blocks on NPM](https://www.npmjs.com/browse/keyword/arrow-block) 120 | 121 | ## Module API 122 | 123 | You can also require `appc-npm` as a module, which is exactly [what the CLI does](bin/appc-npm). 124 | 125 | ## Test [![Dependency Status](https://david-dm.org/FokkeZB/appc-npm.svg)](https://david-dm.org/FokkeZB/appc-npm) [![devDependency Status](https://david-dm.org/FokkeZB/appc-npm/dev-status.svg)](https://david-dm.org/FokkeZB/appc-npm#info=devDependencies) 126 | 127 | To lint and run all tests you need Grunt and a recent version of NPM: 128 | 129 | ``` 130 | $ [sudo] npm install -g grunt 131 | $ [sudo] npm install -g npm 132 | $ npm install 133 | $ npm test 134 | ``` 135 | 136 | To run a specific test by name (without `-test.js`): 137 | 138 | ``` 139 | $ grunt test --test 140 | ``` 141 | 142 | To get a coverage report: 143 | 144 | ``` 145 | $ grunt cover 146 | ``` 147 | 148 | ## Contribute 149 | 150 | To add new types of components, provide a PR with a [type](lib/types), [fixture](test/fixtures) and [test](test). 151 | 152 | ## Issues 153 | 154 | Please report issues and features requests in the repo's [issue tracker](https://github.com/fokkezb/appc-npm/issues). 155 | 156 | ## License 157 | 158 | Distributed under [MIT License](LICENSE). 159 | -------------------------------------------------------------------------------- /assets/appc-npm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var child_process = require('child_process'); 6 | 7 | var OS_WINDOWS = (process.platform === 'win32'); 8 | 9 | install(); 10 | 11 | function install() { 12 | var pkg; 13 | 14 | try { 15 | pkg = JSON.parse(fs.readFileSync('package.json', { 16 | encoding: 'utf-8' 17 | })); 18 | } catch (e) { 19 | die('Could not read package.json'); 20 | } 21 | 22 | var appcNPM = pkg['appc-npm']; 23 | 24 | if (typeof appcNPM !== 'object') { 25 | die('Could not find \'appc-npm\' in package.json'); 26 | } 27 | 28 | var target = findTarget(); 29 | 30 | if (!target) { 31 | die('Could not find project'); 32 | } 33 | 34 | var targetPath = (typeof appcNPM.target === 'object') ? appcNPM.target[target.name] : appcNPM.target; 35 | 36 | if (typeof targetPath !== 'string') { 37 | die('Could not find \'appc-npm.target.' + target.name + '\' or \'appc-npm.target\' in package.json'); 38 | } 39 | 40 | targetPath = path.join(target.base, targetPath); 41 | 42 | var zipFiles = toArray(appcNPM.unzip); 43 | 44 | // unzip 45 | if (zipFiles.length > 0) { 46 | 47 | if (OS_WINDOWS) { 48 | mkdirsSync(targetPath); 49 | } 50 | 51 | unzip(zipFiles, targetPath, configure); 52 | 53 | // copy 54 | } else { 55 | var ignore = toArray(appcNPM.ignore); 56 | ignore.push('appc-npm', '.gitignore', '.npmignore'); 57 | 58 | try { 59 | copySync(__dirname, targetPath, function (fullPath) { 60 | var relPath = fullPath.substr(__dirname.length + 1); 61 | 62 | return ignore.indexOf(relPath) === -1; 63 | }); 64 | 65 | } catch (e) { 66 | die('Could not copy \'' + __dirname + '\' to \'' + targetPath + '\''); 67 | } 68 | 69 | console.log('Copied \'' + __dirname + '\' to \'' + targetPath + '\''); 70 | 71 | configure(); 72 | } 73 | 74 | // configure tiapp.xml and config.json 75 | function configure() { 76 | 77 | // config.json 78 | if (appcNPM.config) { 79 | var configPath = path.join(target.base, 'app', 'config.json'); 80 | var config; 81 | 82 | try { 83 | config = require(configPath); 84 | } catch (e) { 85 | config = {}; 86 | } 87 | 88 | if (!config.dependencies) { 89 | config.dependencies = {}; 90 | } 91 | 92 | for (var id in appcNPM.config) { 93 | if (config.dependencies[id] !== appcNPM.config[id]) { 94 | config.dependencies[id] = appcNPM.config[id]; 95 | } 96 | } 97 | 98 | try { 99 | fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); 100 | 101 | console.log('Updated app/config.json'); 102 | 103 | } catch (e) { 104 | die('Could not write updated app/config.json'); 105 | } 106 | } 107 | 108 | // tiapp.xml 109 | if (appcNPM.tiapp) { 110 | var tiappPath = path.join(target.base, 'tiapp.xml'); 111 | var tiapp; 112 | 113 | try { 114 | tiapp = fs.readFileSync(tiappPath, { 115 | encoding: 'utf-8' 116 | }); 117 | } catch (e) { 118 | die('Could not read tiapp.xml' + e + ' ' + tiappPath); 119 | } 120 | 121 | var add = []; 122 | 123 | appcNPM.tiapp.forEach(function(mod) { 124 | var xml = '' + mod.name + ''; 125 | 126 | var reModule = new RegExp(']+>' + mod.name + ''); 127 | 128 | // update existing 129 | if (reModule.test(tiapp)) { 130 | tiapp = tiapp.replace(reModule, xml); 131 | 132 | // queue to be added 133 | } else { 134 | add.push(xml); 135 | } 136 | }); 137 | 138 | if (add.length > 0) { 139 | var reModules = /(\n)?(\s*<\/modules>)/; 140 | var reModulesClosed = //; 141 | var reTiapp = /(\n)?(\s*<\/ti:app>)/; 142 | 143 | var xml = '\t\t' + add.join('\n\t\t') + '\n'; 144 | var modulesXml = '\n' + xml + '\t'; 145 | 146 | // has .. 147 | if (reModules.test(tiapp)) { 148 | tiapp = tiapp.replace(reModules, '$1' + xml + '$2'); 149 | 150 | // has 151 | } else if (reModulesClosed.test(tiapp)) { 152 | tiapp = tiapp.replace(reModulesClosed, modulesXml); 153 | 154 | // insert before 155 | } else if (reTiapp.test(tiapp)) { 156 | tiapp = tiapp.replace(reTiapp, '$1\t' + modulesXml + '\n$2'); 157 | 158 | // invalid tiapp.xml 159 | } else { 160 | die('Could not update tiapp.xml'); 161 | } 162 | } 163 | 164 | try { 165 | fs.writeFileSync(tiappPath, tiapp); 166 | 167 | console.log('Updated tiapp.xml'); 168 | 169 | } catch (e) { 170 | die('Could not write updated tiapp.xml'); 171 | } 172 | } 173 | 174 | console.log(); 175 | process.exit(0); 176 | } 177 | } 178 | 179 | function unzip(files, targetPath, next) { 180 | var file = files.shift(); 181 | var filePath = path.join(__dirname, file); 182 | var command; 183 | 184 | if (OS_WINDOWS) { 185 | command = '/c cd "' + targetPath + '" && jar xf "' + filePath + '"'; 186 | 187 | } else { 188 | command = 'unzip -o "' + filePath + '" -d "' + targetPath + '"'; 189 | } 190 | 191 | return child_process.exec(command, function(error, stdout, stderr) { 192 | 193 | if (error) { 194 | 195 | var err = 'Could not unzip \'' + filePath + '\': ' + stderr; 196 | 197 | if (OS_WINDOWS) { 198 | var EOL = require('os').EOL; 199 | err += EOL + 'Make sure you have Java JDK installed and added to PATH: ' + EOL + 'http://docs.oracle.com/javase/8/docs/technotes/guides/install/windows_jdk_install.html#BABGDJFH'; 200 | } 201 | 202 | die(err); 203 | 204 | } else { 205 | console.log('Unzipped ' + file); 206 | 207 | if (files.length > 0) { 208 | unzip(files, targetPath, next); 209 | 210 | } else { 211 | next(); 212 | } 213 | } 214 | }); 215 | } 216 | 217 | function findTarget(dir) { 218 | 219 | if (dir) { 220 | 221 | if (fs.existsSync(path.join(dir, 'appc.json'))) { 222 | return { 223 | name: 'arrow', 224 | base: dir 225 | }; 226 | } else if (fs.existsSync(path.join(dir, 'app', 'controllers', 'index.js'))) { 227 | return { 228 | name: 'alloy', 229 | base: dir 230 | }; 231 | } else if (fs.existsSync(path.join(dir, 'tiapp.xml'))) { 232 | return { 233 | name: 'titanium', 234 | base: dir 235 | }; 236 | } 237 | 238 | } else { 239 | dir = __dirname; 240 | } 241 | 242 | dirUp = path.resolve(dir, '..', '..'); 243 | 244 | if (!dirUp || dirUp === dir) { 245 | return; 246 | } 247 | 248 | return findTarget(dirUp); 249 | } 250 | 251 | function die(err) { 252 | console.error(err); 253 | console.error(); 254 | process.exit(1); 255 | } 256 | 257 | function toArray(val) { 258 | 259 | if (typeof val === 'string') { 260 | return [val]; 261 | } else if (Object.prototype.toString.call(val) === '[object Array]') { 262 | return val; 263 | } else { 264 | return []; 265 | } 266 | 267 | } 268 | 269 | /* jshint ignore:start */ 270 | 271 | // https://github.com/jprichardson/node-fs-extra/blob/master/lib/copy/copy-sync.js 272 | function copySync (src, dest, options) { 273 | if (typeof options === 'function' || options instanceof RegExp) { 274 | options = {filter: options} 275 | } 276 | 277 | options = options || {} 278 | options.recursive = !!options.recursive 279 | 280 | // default to true for now 281 | options.clobber = 'clobber' in options ? !!options.clobber : true 282 | 283 | options.filter = options.filter || function () { return true } 284 | 285 | var stats = options.recursive ? fs.lstatSync(src) : fs.statSync(src) 286 | var destFolder = path.dirname(dest) 287 | var destFolderExists = fs.existsSync(destFolder) 288 | var performCopy = false 289 | 290 | if (stats.isFile()) { 291 | if (options.filter instanceof RegExp) performCopy = options.filter.test(src) 292 | else if (typeof options.filter === 'function') performCopy = options.filter(src) 293 | 294 | if (performCopy) { 295 | if (!destFolderExists) mkdirsSync(destFolder) 296 | copyFileSync(src, dest, options.clobber) 297 | } 298 | } else if (stats.isDirectory()) { 299 | if (!fs.existsSync(dest)) mkdirsSync(dest) 300 | var contents = fs.readdirSync(src) 301 | contents.forEach(function (content) { 302 | copySync(path.join(src, content), path.join(dest, content), {filter: options.filter, recursive: true}) 303 | }) 304 | } else if (options.recursive && stats.isSymbolicLink()) { 305 | var srcPath = fs.readlinkSync(src) 306 | fs.symlinkSync(srcPath, dest) 307 | } 308 | } 309 | 310 | // https://github.com/jprichardson/node-fs-extra/blob/master/lib/copy/copy-file-sync.js 311 | function copyFileSync (srcFile, destFile, clobber) { 312 | 313 | if (fs.existsSync(destFile) && !clobber) { 314 | throw Error('EXIST') 315 | } 316 | 317 | // simplified to work with vanilla fs 318 | fs.writeFileSync(destFile, fs.readFileSync(srcFile)); 319 | } 320 | 321 | // https://github.com/jprichardson/node-fs-extra/blob/master/lib/mkdirs/mkdirs.js 322 | var o777 = parseInt('0777', 8) 323 | 324 | function mkdirsSync (p, opts, made) { 325 | if (!opts || typeof opts !== 'object') { 326 | opts = { mode: opts } 327 | } 328 | 329 | var mode = opts.mode 330 | var xfs = opts.fs || fs 331 | 332 | if (mode === undefined) { 333 | mode = o777 & (~process.umask()) 334 | } 335 | if (!made) made = null 336 | 337 | p = path.resolve(p) 338 | 339 | try { 340 | xfs.mkdirSync(p)//, mode)