Skip to content

Commit

Permalink
Add api and cli for module tagging
Browse files Browse the repository at this point in the history
  • Loading branch information
doug-wade committed Jun 24, 2016
1 parent a992842 commit a92164d
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 92 deletions.
95 changes: 4 additions & 91 deletions packages/react-server-gulp-module-tagger/index.js
Original file line number Diff line number Diff line change
@@ -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__"
Expand All @@ -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 (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(opts){
return colors[hash(opts.name)];
}
})();
3 changes: 2 additions & 1 deletion packages/react-server-gulp-module-tagger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 25 additions & 0 deletions packages/react-server-module-tagger/README.md
Original file line number Diff line number Diff line change
@@ -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`).
129 changes: 129 additions & 0 deletions packages/react-server-module-tagger/index.js
Original file line number Diff line number Diff line change
@@ -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 (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(opts){
return colors[hash(opts.name)];
}
})();
30 changes: 30 additions & 0 deletions packages/react-server-module-tagger/package.json
Original file line number Diff line number Diff line change
@@ -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 <doug.wade@redfin.com>",
"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"
}
}
32 changes: 32 additions & 0 deletions packages/react-server-module-tagger/test.js
Original file line number Diff line number Diff line change
@@ -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);
});

0 comments on commit a92164d

Please sign in to comment.