package/bin/catty000755 0000002743 3560116604 011110 0ustar00000000 000000 #!/usr/bin/env node var opts = require("nomnom") .help("Usage: catty [flags] input [output]") .option('follow', { abbr: "f", flag: true }) .option('dir', { abbr: "d" }) .option('global', { flag: true, abbr: 'g' }) .option('deps', { }) .parse(); var catty = require("../")(opts); var input = opts._[0]; var output = opts._[1]; var keyCommands = []; if (opts.follow) { initKeyCommands(); } if (opts.dir) { opts.dir.split(',').forEach(function(d) { catty.addLibrary(d); }); } catty.cat([input], output); function initKeyCommands() { process.stdin.resume(); process.stdin.setEncoding('utf8'); process.stdin.setRawMode(true); process.stdin.on('data', function(char) { if (char == '\3') { process.exit(); } else if (char in keyCommands) { runKeyCommand(char); } else { process.stdout.write(char); } }); addKeyCommand('q', 'quit', function() { process.exit(); }); addKeyCommand('h', 'list commands', function() { listKeyCommands(); }); } function addKeyCommand(flag, description, callback) { keyCommands[flag] = {callback: callback, description: description}; } function listKeyCommands() { var msg = "Key commands:"; keyCommands.forEach(function(cmd, k) { var cmd = keyCommands[k]; msg += "\n " + k + ": " + cmd.description; }); console.log(msg); } function runKeyCommand(c) { var obj = keyCommands[c]; if (obj) { obj.callback(); } } package/catty.js000644 0000024636 3560116604 010755 0ustar00000000 000000 var fs = require('fs'); var path = require('path'); var _ = require('underscore'); var catty = new Catty(); // regex for comments like: // /* @requires name1, name2, name3 */ // (Comments may span multiple lines, commas are optional) var REQUIRES_RXP = /\/\*+\s*@requires?\b([\s,;_0-9A-Za-z.-]+)\s*\*+\/\s*\n?/g; function Catty(opts) { var prepended = "", externals = []; knownFileIndex = {}, // paths of known js files indexed by basename watchedFiles = {}, // SourceFile objects indexed by basename jobs = [], addedDeps = []; opts = _.extend({ global: false, follow: false }, opts || {}); this.internal = { // expose internal functions for unit testing parseDeps: parseDeps, stripBOM: stripBOM }; // @deps array or comma-separated list of depencencies to insert into the // root document(s) this.addDeps = function(deps) { if (_.isString(deps)) { deps = deps.split(','); } addedDeps = _.union(addedDeps, deps); return this; }; // @arg name of an external dependency, which shouldn't be bundled this.external = function(arg) { externals = _.isArray(arg) ? arg : _.toArray(arguments); return this; }; // @js JS string to insert at the beginning of the concatenated files, but // inside the module closure (could contain variable definitions) this.prepend = function(js) { try { // eval(js); // catch syntax errors prepended = js; } catch(e) { console.error("[prepend] Invalid JavaScript: ", js); } return this; }; // @path A directory containing JavaScript source files // (subdirectories are also indexed) this.addLibrary = function(path) { if (!dirExists(path)) { die("Not a valid directory: " + path); } findSourceFiles(path).forEach(indexFile); return this; }; // Compile JS source files this.cat = function(src, dest) { var job; try { job = new CattyJob(src, dest); if (addedDeps.length > 0) { job.addDeps(addedDeps); } } catch(e) { die(e.message); } job.run(); jobs.push(job); // save job so it can be run again, if monitoring files return this; }; function runJobs() { jobs.forEach(function(job) { job.run(); }); } function getNode(key) { var node = watchedFiles[key]; if (!node) { throw new Error("Missing dependency: " + key); } return node; } function sortNodes(nodes) { var startId = 0, len = nodes.length, sorted = {}, nodeName, i, startNode, reqId; while (startId < len-1) { startNode = nodes[startId]; reqId = -1; if (startNode.name() in sorted === false) { for (i=startId+1; i 0) { nodes.splice(startId, 1); nodes.splice(reqId, 0, startNode); } else { startId++; } sorted[startNode.name()] = true; } } // Add file to index of known files; // Assumes @path exists. // function indexFile(path) { var name = getFileInfo(path).basename; if (!name) { die("Invalid path: " + path); } else if (name in knownFileIndex === false) { knownFileIndex[name] = path; } else if (knownFileIndex[name] !== path) { console.error("File name collision."); console.error("Using:", knownFileIndex[name]); console.error("Ignoring:", path); } return name; } function SourceFile(path) { var info = getFileInfo(path), _deps = [], _insertedDeps = [], _js = ""; if (!info.is_file || info.ext != '.js') { die("Invalid source file: " + path); } watchedFiles[info.basename] = this; if (opts.follow) { startMonitoring(); } findDeps(); this.name = function() { return info.basename; }; this.getContent = function() { return _js; }; this.getDeps = function() { return _deps; }; this.insertDeps = function(deps) { _insertedDeps = deps; updateDeps(_deps); }; this.requiresFile = function(targName, visited) { visited = visited || {}; visited[this.name()] = true; var reqs = this.getDeps(); if (_.contains(reqs, targName)) { return true; } for (var i=0; i 0 && js !== _js; if (changed) { _js = js; updateDeps(parseDeps(js)); } return changed; } function updateDeps(deps) { deps = _.union(deps, _insertedDeps); deps = _.difference(deps, externals); deps.forEach(addDependency); _deps = deps; } function addDependency(key) { if (key in knownFileIndex === false) { throw new Error("Unknown dependency in " + path + " -- " + key); } if (key in watchedFiles === false) { new SourceFile(knownFileIndex[key]); } } function onChange(err) { if (err) { console.error(err.message); } else { console.error("Re-catting -- change in " + path); runJobs(); // TODO: only run jobs that use this the changed source file } } function startMonitoring() { var timeout = null; fs.watch(path, function(evt) { if (evt == "change" || evt == "rename") { // Use a timeout to make sure file has actually changed // (Had problems in os x) timeout && clearTimeout(timeout); timeout = setTimeout(function() { try { if (findDeps()) { onChange(); } } catch(e) { onChange(e); } }, 150); } }); } } // SourceFile function CattyJob(src, dest) { var roots = []; var useStdout = !dest || dest == '-' || dest == '/dev/stdout'; var inFiles; if (_.isString(src)) { inFiles = [src]; } else if (_.isArray(src)) { inFiles = src; } else { die("Invalid input file(s): " + src); } if (opts.follow && useStdout) { die("-f option is not compatible with output to stdout"); } roots = inFiles.map(function(ifile) { ifile = path.join(ifile); // Update slashes on Windows if (ifile == dest) die("Tried to overwrite a source file: " + ifile); if (!fileExists(ifile)) die("Source file not found: " + ifile); var name = indexFile(ifile); return new SourceFile(ifile); }); // return list of all deps reached by list of deps function findDeps(newDeps, foundDeps) { return newDeps.reduce(function(memo, key) { if (memo.indexOf(key) == -1) { memo.push(key); findDeps(getNode(key).getDeps(), memo); } return memo; }, foundDeps || []); } function concatenate() { var keys = roots.map(function(node) {return node.name();}); var nodes = findDeps(keys).map(getNode); sortNodes(nodes); return nodes.map(function(node) { return node.getContent(); }).join('\n\n'); }; function stripComments(js) { return js.replace(REQUIRES_RXP, ''); } function addClosure(js) { return "(function(){\n" + js + "\n}());\n"; } function bundle() { var js = ""; if (prepended) { js += prepended + '\n'; } js += concatenate(); js = stripComments(js); if (!opts.global) { js = addClosure(js); } return js; } this.addDeps = function(deps) { roots.forEach(function(node) {node.insertDeps(deps);}); }; this.run = function() { var js, dirname, err; try { js = bundle(); } catch(e) { err = e; } if (_.isFunction(dest)) { dest(err, js); } else if (err) { // Print message, don't exit (let user correct dependency problems // when monitoring files). console.error(e.message); } else if (useStdout) { console.log(js); } else if (_.isString(dest)) { // check that dir (still) exists dirname = path.dirname(dest); if (!dirExists(dirname)) { die("Destination directory not found: " + dirname); } fs.writeFileSync(dest, js); console.error("Wrote " + dest); } }; } // CattyJob } // Catty function stripBOM(str) { if (str && str.charCodeAt(0) === 0xFEFF) { str = str.slice(1); } return str; } function parseDeps(js) { var fileRxp = /\*?[_0-9a-z](?:[.-]?[_0-9a-z])*/ig, deps = [], match, match2; while (match = REQUIRES_RXP.exec(js)) { while (match2 = fileRxp.exec(match[1])) { deps.push(match2[0]); } } return deps; } function findSourceFiles(dirPath) { var results = walkSync(dirPath); return results.filter(function(filePath) { return /\.js$/.test(filePath); }); } function dirExists(path) { return !!getFileInfo(path).is_dir; } function fileExists(path) { return !!getFileInfo(path).is_file; } function getFileInfo(p) { var info = {}, stat; try { stat = fs.statSync(p); info.exists = true; info.is_file = stat.isFile(); info.is_dir = stat.isDirectory(); info.directory = path.dirname(p); } catch(e) {}; if (info.is_file) { info.ext = path.extname(p); info.filename = path.basename(p); info.basename = info.filename.substr(0, info.filename.length - info.ext.length); } return info; } function walkSync(dir, memo) { memo = memo || []; var list = fs.readdirSync(dir); list.forEach(function(file) { var filepath = path.join(dir, file); var stat = fs.statSync(filepath); if (stat && stat.isDirectory()) { walkSync(filepath, memo); } else { memo.push(filepath); } }); return memo; } function die(msg) { if (msg) console.error(msg); process.exit(1); } module.exports = function(opts) { return new Catty(opts); // allow multiple instances with different options }; _.extend(module.exports, catty); package/package.json000644 0000001301 3560116604 011541 0ustar00000000 000000 { "name": "catty", "version": "0.1.0", "description": "Mapshaper's build tool", "keywords": [ "mapshaper" ], "author": "Matthew Bloch ", "main": "./catty.js", "dependencies": { "underscore": "~1.7", "nomnom": "~1.8" }, "bin": { "catty": "./bin/catty" }, "devDependencies": { "mocha": "^6.2.1" }, "scripts": { "test": "mocha --check-leaks -R dot" }, "repository": { "type": "git", "url": "https://github.com/mbloch/catty.git" }, "files": [ "/bin/catty", "/catty.js" ], "license": "MIT", "bugs": { "url": "https://github.com/mbloch/catty/issues" }, "homepage": "https://github.com/mbloch/catty" } package/README.md000644 0000003072 3560116604 010541 0ustar00000000 000000 ## Catty ## Catty is the source file concatenator for [Mapshaper](https://github.com/mbloch/mapshaper). Some features: * Each source file lists its dependencies in a formatted comment (see below). There is no manifest, unlike some other tools. * Concatenated files are (optionally) wrapped in a self-executing function, to protect the global namespace. * Catty can monitor source files and regenerate output files when a required source file changes. ### Command line tool ### Usage: `$ catty [options] input output` Options: - `-f` Monitor source files, re-cat when something changes. - `-g` Don't wrap source files in an immediate function. - `-d` Comma-separated list of directories to monitor - `-h` Print help message Example: `$ catty -g -d src,lib src/input.js dist/output.js` ### Node module ### **Example** ``` require('catty')({global: true}) .addLibrary('src') .addLibrary('lib') .cat('src/input.js', 'dist/output.js'); ``` **Methods** `#cat(input, output)` Concatenate an input file with its dependencies. `output` may be a filename or a Node-style callback. If `output` is empty or contains a hyphen, catty sends its output to `stdout`. `#addLibrary(dir)` Give catty a directory to search for dependencies. `#prepend(JS)` Insert JavaScript before concatenated code. `#addDeps(deps)` Inject one or more additional dependencies into the input file (comma-sep. list). ### Comment format ### **Examples** ``` /* @requires mapshaper-innerlines mapshaper-endpoints mapshaper-dataset-utils */ ``` ``` /* @requires mapshaper-shapes, mapshaper-shape-geom */ ``` ls package/LICENSE.txt000644 0000002070 3560116604 011102 0ustar00000000 000000 The MIT License (MIT) Copyright (c) 2015 Matthew Bloch 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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.