diff --git a/packages/react-server-gulp-module-tagger/index.js b/packages/react-server-gulp-module-tagger/index.js index ca5627d43..e2aa2b04a 100644 --- a/packages/react-server-gulp-module-tagger/index.js +++ b/packages/react-server-gulp-module-tagger/index.js @@ -1,5 +1,6 @@ var replace = require("gulp-replace") , forEach = require("gulp-foreach") +, loggerSpec = require("react-server-module-tagger") // This pattern matches either of these: // - "__LOGGER__" @@ -9,99 +10,11 @@ var isWindows = ('win32' === process.platform) , THIS_MODULE = isWindows ? /(?:[^\\]+\\node_modules\\)?react-server-gulp-module-tagger\\index\.js$/ : /(?:[^\/]+\/node_modules\/)?react-server-gulp-module-tagger\/index\.js$/ -, BASE_PATH = module.filename.replace(THIS_MODULE,'') -module.exports = function(config){ +module.exports = function(config) { config || (config = {}); + config.basePath = module.filename.replace(THIS_MODULE,''); return forEach(function(stream, file){ return stream.pipe(replace(REPLACE_TOKEN, loggerSpec.bind({file, config}))); - }) + }); } - -var loggerSpec = function(fullMatch, optString){ - var fn = this.file.path - , trim = this.config.trim || '' - , opts = {} - - if (fn.indexOf(BASE_PATH) !== 0) { - throw new Error("Unable to handle "+REPLACE_TOKEN+" for "+fn); - } - - if (optString) { - // The slash replacement here is so we don't choke on example - // loggers in comments. - opts = new Function("return "+optString.replace(/^\/\//mg,''))(); // eslint-disable-line no-new-func - } - - opts.name = getName (fn, opts, trim); - opts.color = getColor (fn, opts); - - return JSON.stringify(opts); -} - -var getName = function(fn, opts, trim){ - var slashPattern = isWindows - ?/\\/g - :/\//g - var name = fn.substring(BASE_PATH.length+trim.length, fn.length) - .replace(/\.jsx?$/, '') - .replace(slashPattern,'.') - if (opts.label) { - name += '.'+opts.label - } - return name; -} - -var getColor = (function(){ - - // ANSI escape sequence on the server. - // CSS rgb(...) color in the browser. - var makeColor = function(r,g,b){ - return { - server: 16 + r*36 + g*6 + b, - client: "rgb("+[ - (r*42.5)|0, - (g*42.5)|0, - (b*42.5)|0, - ].join(',')+")", - } - } - - // This produces a list of 24 colors that are distant enough from each - // other to be visually distinct. It's also, conveniently, the same - // palette client side and server side. - var colors = []; - for (var r = 1; r < 6; r+=2) { - for (var g = 1; g < 6; g+=2) { - for (var b = 1; b < 6; b+=2) { - if (r !== g || g !== b) { // No gray. - colors.push(makeColor(r,g,b)); - } - } - } - } - - - // Just want a fairly well distributed deterministic mapping. - // - // Adapted from: - // http://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript-jquery - var hash = function(str){ - var hash = 0, i, chr, len; - if (str.length === 0) return hash; - for (i = 0, len = str.length; i < len; i++) { - chr = str.charCodeAt(i); - hash = ((hash << 5) - hash) + chr; - hash |= 0; // Convert to 32bit integer - } - - len = colors.length; - - // Positive mod. - return (hash%len+len)%len; - } - - return function(fn, opts){ - return colors[hash(opts.name)]; - } -})(); diff --git a/packages/react-server-gulp-module-tagger/package.json b/packages/react-server-gulp-module-tagger/package.json index 0dce5aee8..0ca0295db 100644 --- a/packages/react-server-gulp-module-tagger/package.json +++ b/packages/react-server-gulp-module-tagger/package.json @@ -16,7 +16,8 @@ "dependencies": { "gulp-foreach": "0.1.0", "gulp-plumber": "^1.1.0", - "gulp-replace": "0.5.2" + "gulp-replace": "0.5.2", + "react-server-module-tagger": "0.0.1" }, "devDependencies": { "ava": "^0.15.2", diff --git a/packages/react-server-module-tagger/README.md b/packages/react-server-module-tagger/README.md new file mode 100644 index 000000000..ee662c30e --- /dev/null +++ b/packages/react-server-module-tagger/README.md @@ -0,0 +1,25 @@ +# react-server-module-tagger + +A function for tagging [react-server](https://www.npmjs.com/package/react-server) +logger instances with information about the module they're being used in. + +To transpile your source for use with +[React Server](https://www.npmjs.com/package/react-server), install gulp and the plugin + +```shell +npm i -D gulp react-server-module-tagger +``` + +Then require and call the function. The tagger expects to have config and file +data on its prototype, so use `.bind`. + +```javascript +const tagger = require('react-server-module-tagger'); +const filepath = 'path/to/my/output.js'; +const optString = '({label:"foo"})' +const moduleTag = tagger.bind({ file: { path: filepath }, config: { trim: 'path/to'} })(filepath, optString)); +``` + +returns a logger instance that will have consistent coloring on the server and +the client, and that has a human-friendly, readable name that easily maps to +the file tree (in this example `components.my-feature.foo`). diff --git a/packages/react-server-module-tagger/index.js b/packages/react-server-module-tagger/index.js new file mode 100644 index 000000000..7eec16c5c --- /dev/null +++ b/packages/react-server-module-tagger/index.js @@ -0,0 +1,129 @@ +var isWindows = ('win32' === process.platform); + +/** + * A util function for tagging modules. Expects to find data on its prototype + * as follows + * { + * file: { + * path: the path to the file to tag + * } + * config: { + * trim: the string to trim off the front of the logger name + * } + * } + * @example + * const file = '/path/to/my/file.js'; + * loggerSpec.bind({file, { trim: 'path.to.' }})(file, '({label: "foo"})') + * // returns '{\"label\":\"foo\",\"name\":\"my.file\",\"color\":{\"server\":87,\"client\":\"rgb(42,212,212)\"}}' + * @param {String} fullMatch May also be provided as this.file.path, the path to the file + * @param {String} optString The label to add to the module tag, in the form '({label:"$label"})' + * @return {String} A json object containing a module identifier + */ +module.exports = function(fullMatch, optString){ + var fn = this.file && this.file.path ? this.file.path : fullMatch + , trim = this.config.trim || '' + , basePath = this.config.basePath || '' + , opts = {} + + if (fn.indexOf(basePath) !== 0) { + throw new Error("Unable to handle " + basePath + " for " + fn); + } + + if (optString) { + // The slash replacement here is so we don't choke on example + // loggers in comments. + opts = new Function("return "+optString.replace(/^\/\//mg,''))(); // eslint-disable-line no-new-func + } + + opts.name = getName (fn, opts, trim, basePath); + opts.color = getColor (fn, opts); + + return JSON.stringify(opts); +} + +/** + * Gets the name of a logger from its filepath. + * @example + * getName( + * 'my-component', + * { label: 'sub' }, + * 'my-project.src', + * 'my-project/src/components/my-component.js' + * ) // returns "components.my-component" + * @param {String} fn filename + * @param {Object} opts { label: 'Optional logger label' } + * @param {String} trim The leading portion of the name to remove. + * @param {String} basePath The path to the file + * @return {String} The logger name, e.g. my-project.components.my-component + */ +var getName = function(fn, opts, trim, basePath){ + var slashPattern = isWindows + ?/\\/g + :/\//g + var name = fn.substring(basePath.length+trim.length, fn.length) + .replace(/\.jsx?$/, '') + .replace(slashPattern,'.') + if (opts.label) { + name += '.'+opts.label + } + return name; +} + +/** + * Gets isomorphic color objects from filenames. + * @param {String} filename + * @return {Object} An isomorphic color object in the form {"color":{"server":147,"client":"rgb(127,127,212)"}} + */ +var getColor = (function(){ + + // ANSI escape sequence on the server. + // CSS rgb(...) color in the browser. + var makeColor = function(r,g,b){ + return { + server: 16 + r*36 + g*6 + b, + client: "rgb("+[ + (r*42.5)|0, + (g*42.5)|0, + (b*42.5)|0, + ].join(',')+")", + } + } + + // This produces a list of 24 colors that are distant enough from each + // other to be visually distinct. It's also, conveniently, the same + // palette client side and server side. + var colors = []; + for (var r = 1; r < 6; r+=2) { + for (var g = 1; g < 6; g+=2) { + for (var b = 1; b < 6; b+=2) { + if (r !== g || g !== b) { // No gray. + colors.push(makeColor(r,g,b)); + } + } + } + } + + + // Just want a fairly well distributed deterministic mapping. + // + // Adapted from: + // http://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript-jquery + var hash = function(str){ + var hash = 0, i, chr, len; + if (str.length === 0) return hash; + for (i = 0, len = str.length; i < len; i++) { + chr = str.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; // Convert to 32bit integer + } + + len = colors.length; + + // Positive mod. + return (hash%len+len)%len; + } + + return function(fn, opts){ + return colors[hash(opts.name)]; + } +})(); diff --git a/packages/react-server-module-tagger/package.json b/packages/react-server-module-tagger/package.json new file mode 100644 index 000000000..2d5258965 --- /dev/null +++ b/packages/react-server-module-tagger/package.json @@ -0,0 +1,30 @@ +{ + "name": "react-server-module-tagger", + "version": "0.0.1", + "description": "calculates module level config", + "main": "index.js", + "scripts": { + "test": "ava test.js", + "clean": "del index.js npm-debug.log*" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/redfin/react-server.git" + }, + "keywords": [ + "react-server" + ], + "author": "Doug Wade ", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/redfin/react-server/issues" + }, + "homepage": "https://github.com/redfin/react-server#readme", + "devDependencies": { + "ava": "^0.15.2", + "babel-cli": "^6.9.0", + "babel-preset-es2015": "^6.9.0", + "babel-preset-stage-0": "^6.5.0", + "del-cli": "^0.2.0" + } +} diff --git a/packages/react-server-module-tagger/test.js b/packages/react-server-module-tagger/test.js new file mode 100644 index 000000000..55f99cec9 --- /dev/null +++ b/packages/react-server-module-tagger/test.js @@ -0,0 +1,32 @@ +import test from 'ava'; +import loggerSpec from '.'; + +test('creates a module tag', t => { + const expected = '{\"name\":\"foo.bar\",\"color\":{\"server\":73,\"client\":\"rgb(42,127,127)\"}}'; + + const file = 'foo/bar'; + const config = {}; + const actual = loggerSpec.bind({file, config})(file); + + t.is(expected, actual); +}); + +test('trims prefix from module tag name', t => { + const expected = '{\"name\":\"quux\",\"color\":{\"server\":229,\"client\":\"rgb(212,212,127)\"}}'; + + const file = 'baz/quux'; + const config = { trim: 'baz.' }; + const actual = loggerSpec.bind({file, config})(file); + + t.is(expected, actual); +}); + +test('adds labels', t => { + const expected = '{\"label\":\"foo\",\"name\":\"has.label.foo\",\"color\":{\"server\":131,\"client\":\"rgb(127,42,42)\"}}'; + + const file = 'has/label'; + const config = {}; + const actual = loggerSpec.bind({file, config})(file, '({label: "foo"})'); + + t.is(expected, actual); +});