Coder Social home page Coder Social logo

krakenjs / post-robot Goto Github PK

View Code? Open in Web Editor NEW
729.0 27.0 94.0 10.49 MB

Cross domain post-messaging on the client side using a simple listener/client pattern.

License: Apache License 2.0

JavaScript 99.50% Shell 0.10% HTML 0.40%
popup-messaging popup-windows popup post-messaging postmessaging promise hacktoberfest

post-robot's Introduction

post-robot [:]-\-<

build status code coverage npm version

Cross domain post-messaging on the client side, using a simple listener/client pattern.

Send a message to another window, and:

Serialization

post-robot will serialize and deserialize the following data types in messages:

  • Objects, arrays, strings, numbers, booleans, null
    • Note: this includes any JSON-serializable types
  • Functions
  • Promises
    • Note: deserialized promises will be instances of ZalgoPromise
  • Error objects
    • e.g. new Error("This error will self-destruct in 10, 9, 8...")
  • Regex objects
    • e.g. /[a-zA-Z0-9]*/

Simple listener and sender

// Set up a listener

postRobot.on("getUser", function (event) {
  // Have it return some data to the calling window

  return {
    id: 1234,
    name: "Zippy the Pinhead",

    // Yep, we're even returning a function to the other window!

    logout: function () {
      return $currentUser.logout();
    },
  };
});
// Call the listener, on a different window, on a different domain

postRobot
  .send(someWindow, "getUser", { id: 1337 })
  .then(function (event) {
    var user = event.data;

    console.log(event.source, event.origin, "Got user:", user);

    // Call the user.logout function from the other window!

    user.logout();
  })
  .catch(function (err) {
    // Handle any errors that stopped our call from going through

    console.error(err);
  });

Listener with promise response

postRobot.on("getUser", function (event) {
  return getUser(event.data.id).then(function (user) {
    return {
      name: user.name,
    };
  });
});

One-off listener

postRobot.once("getUser", function (event) {
  return {
    name: "Noggin the Nog",
  };
});

Cancelling a listener

var listener = postRobot.on("getUser", function (event) {
  return {
    id: event.data.id,
    name: "Zippy the Pinhead",
  };
});

listener.cancel();

Listen for messages from a specific window

postRobot.on("getUser", { window: window.parent }, function (event) {
  return {
    name: "Guybrush Threepwood",
  };
});

Listen for messages from a specific domain

postRobot.on("getUser", { domain: "http://zombo.com" }, function (event) {
  return {
    name: "Manny Calavera",
  };
});

Set a timeout for a response

postRobot
  .send(someWindow, "getUser", { id: 1337 }, { timeout: 5000 })
  .then(function (event) {
    console.log(event.source, event.origin, "Got user:", event.data.name);
  })
  .catch(function (err) {
    console.error(err);
  });

Send a message to a specific domain

postRobot
  .send(someWindow, "getUser", { id: 1337 }, { domain: "http://zombo.com" })
  .then(function (event) {
    console.log(event.source, event.origin, "Got user:", event.data.name);
  });

Async / Await

postRobot.on("getUser", async ({ source, origin, data }) => {
  let user = await getUser(data.id);

  return {
    id: data.id,
    name: user.name,
  };
});
try {
  let { source, origin, data } = await postRobot.send(someWindow, `getUser`, {
    id: 1337,
  });
  console.log(source, origin, "Got user:", data.name);
} catch (err) {
  console.error(err);
}

Secure Message Channel

For security reasons, it is recommended that you always explicitly specify the window and domain you want to listen to and send messages to. This creates a secure message channel that only works between two windows on the specified domain:

postRobot.on(
  "getUser",
  { window: childWindow, domain: "http://zombo.com" },
  function (event) {
    return {
      id: event.data.id,
      name: "Frodo",
    };
  }
);
postRobot
  .send(someWindow, "getUser", { id: 1337 }, { domain: "http://zombo.com" })
  .then(function (event) {
    console.log(event.source, event.origin, "Got user:", event.data.name);
  })
  .catch(function (err) {
    console.error(err);
  });

Functions

Post robot lets you send across functions in your data payload, fairly seamlessly.

For example:

postRobot.on("getUser", function (event) {
  return {
    id: event.data.id,
    name: "Nogbad the Bad",

    logout: function () {
      currentUser.logout();
    },
  };
});
postRobot.send(myWindow, "getUser", { id: 1337 }).then(function (event) {
  var user = event.data;

  user.logout().then(function () {
    console.log("User was logged out");
  });
});

The function user.logout() will be called on the original window. Post Robot transparently messages back to the original window, calls the function that was passed, then messages back with the result of the function.

Because this uses post-messaging behind the scenes and is therefore always async, user.logout() will always return a promise, and must be .then'd or awaited.

Parent to popup messaging

Unfortunately, IE blocks direct post messaging between a parent window and a popup, on different domains.

In order to use post-robot in IE9+ with popup windows, you will need to set up an invisible 'bridge' iframe on your parent page:

   [ Parent page ]

+---------------------+          [ Popup ]
|        xx.com       |
|                     |      +--------------+
|  +---------------+  |      |    yy.com    |
|  |    [iframe]   |  |      |              |
|  |               |  |      |              |
|  | yy.com/bridge |  |      |              |
|  |               |  |      |              |
|  |               |  |      |              |
|  |               |  |      |              |
|  |               |  |      +--------------+
|  +---------------+  |
|                     |
+---------------------+

a. Use the special ie build of post-robot: dist/post-robot.ie.js.

b. Create a bridge path on the domain of your popup, for example http://yy.com/bridge.html, and include post-robot:

<script src="http://yy.com/js/post-robot.ie.js"></script>

c. In the parent page on xx.com which opens the popup, include the following javascript:

<script>
  postRobot.bridge.openBridge("http://yy.com/bridge.html");
</script>

Now xx.com and yy.com can communicate freely using post-robot, in IE.

post-robot's People

Contributors

amyegan avatar bluepnume avatar borodovisin avatar bryclee avatar chichisr avatar christophior avatar dackmin avatar dtjones404 avatar mnicpt avatar mrvictorios avatar mstuart avatar pragatheeswarans avatar raymondmik avatar ryanwatkins avatar sandinosaso avatar westeezy avatar wildlingjill avatar wsbrunson avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

post-robot's Issues

IE - access is denied

Hi,

I get an access denied error in an angular application on Internet Explorer 11, chrome works fine. The docs say:

Unfortunately, IE blocks direct post messaging between a parent window and a popup, on different domains.

But we are not using popups (I think you mean window.open(...), right?). So, is it always necessary to use the suggested "bridge system" when running on IE?

Add window validation on send function

https://github.com/krakenjs/post-robot/blob/master/src/interface/client.js#L112

Was a stupid mistake but i didn't append my iframe to the DOM till after postRobot.send and got an error Expected options.window from the request function. Would be nice to validate that window is not undefined and maybe some other properties to validate it is an actual window and if not throw a better error. I'll can submit a PR if you tell me what other properties to validate.

image

Disable logs (and changing log levels)

tldr: what is the recommended way of disabling logging, and changing log level?


I'm trying to disable the log to the browser console. I didn't see any documentation for this - did I miss it?

I'm able to change the log level to 'error' (which is probably a better idea than disabling it entirely). Is this the recommended way of doing it?

var postRobot = require('post-robot');
postRobot.CONFIG.LOG_LEVEL = 'error';

Thanks

eval in post-robot.js

we are using post-robot.js for react chat application, however a csp breach has been reported to us for the use of eval. When we analysed the eval is being used in post-robot.js. we are using post-robot.js as node module, can we fix this in the library so that we can update the module and resolve the CSP issues?? Or is there any other way round for this?? Issue is at Line : 793 post-robot.js.

Multiple listeners on the same event

Hello!

I would like to have multiple listeners on the same event. e.g:

const a = () => {
    channel.on('some:event', event => {
        // do A...
    });
}
const b = () => {
    channel.on('some:event', event => {
        // do B...
    });
}

The above example will thrown an error Request listener already exists for. Is this the real intention?

awaitRemoteBridgeForWindow returns incorrect iframe

When a parent page has multiple iframes on the same domain all using postRobot, awaitRemoteBridgeForWindow will fail to return the correct bridge frame.

Our application opens up communication utility frames to the same domain, but also uses Zoid to open a popup to that domain. On IE that leads to postRobot attempting to use a non-bridge frame as a bridge. This leads to error: No handler found for post message: postrobot_open_tunnel from ...

Question about mulitple iframes support

I am working on a web application that it needs to support multiple 3rd party iframe to be embedded in the same page, and need communication between the parent page and those iframes. they should all have the same set of API calls from the parent to child iframe and vice versa.

for example, parent page will send some data to each child iframe and each child iframe may send certain data back.

From what I have read about post-robot so far, do I have to make all the event name unique? if so, that brings another problem that how does the child iframe (3rd part code) know what's the unique event identifier is? Do I have to put it in the iframe URL or set up another postMessage event to pass it?

Does post-robot have some feature which can make the event listen iframe scoped?

Thank you very much for you help.

Cannot listen to same event twice

I am currently doing something along the lines:

postRobot.on('myEventName', function() {
	// Do something
});

// In some other part of the codebase

postRobot.on('myEventName', function() {
	// Do something else
});

This throws Request listener already exists for <eventName> on domain *.

Looking in the source, I can see it's possible to override the previous listener.

What if I want two listeners for the same event? This is necessary keep separation of concerns.

Thanks.

Enforce Security by default

Can we make the domain configuration item a mandatory configuration that occurs before init? I think this is really important, as it can really cause a lot of malicious issues if people use it and don't maintain it.

Expected message to have sourceDomain

The newer version of the lib is giving me the following error:

image

I had to fix the version on "9.0.0" so it would come back working again, didn't find any help on the docs.

Cancel function on the event listener

If I initialize the listener in this way, postRobot.listener and register more than one on,

var listener = postRobot.listener({ window: childWindow, domain: 'http://zombo.com' });

listener.on('getUser', function(event) {
    return {
        id:   event.data.id,
        name: 'Frodo'
    };
});

listener.on('getManager', function(event) {
    return {
        id:   event.data.id,
        name: 'Erik'
    };
});

it would be great if I can simply call

listener.cancel()

to cancel all the event listeners, rather than I have to store the listener handler of each on function and cancel them one by one.

Stop distributing .babelrc

Your current NPM package includes .babelrc, despite being pre-compiled.

This is an issue because webpack/babel in the project using post-robot then parses your .babelrc and explodes if your required dependencies are not installed.

At the same time, you might want to check the NPM package for other items that might not be interesting, like the setup-files for gulp and eslint.

Typings

Are there any typings (TypeScript) or plans to provide them? Can't find anything on that topic...

Illegal modification of built-in Buffer class

Uncaught TypeError: Cannot assign to read only property 'toString' of object '[object Object]'

Buffer.prototype.toString = function toString () {
  var length = this.length | 0
  if (length === 0) return ''
  if (arguments.length === 0) return utf8Slice(this, 0, length)
  return slowToString.apply(this, arguments)
}

Seems to be conflicting with a browser security extension.

Communication between iframes located in the same window

We want to send msg between iframes located in the same window, is this supported? We suppose that not because this line: if (win === window) throw new Error("Attemping to send message to self");.

Is that correct? In this case, are you working for add this funcionallity?

No Response from Window - clean up

Hi I am getting "No Response from Window - clean up" error in my PayPal integration. I search and can't find the error in google except for one of the source code in this repo. Can you please let me know what this error means and when it throws this error?

Basically for some customers PayPal window opens for brief time and then closes. I can't replicate but I know this is happening from my log and also customers are complaining.

Thanks.

support for retry on handshake failure

Hi krakenjs,
I am working on a hybrid project, where we have a react host app hosting an old app (in angular ) in an iframe.

However, the angular app (in the iframe) is a complex application which takes an indefinite time (depends on the client machine and the network) to become fully functional.

It would be nice if post-robot could handle a scenario like.

postRobot.send(iframe, {data:'hello'}, {timeout: 1000, retry:3});

where retry is the max retry( the request will only be sent once).

I could not find any similar functionality in post-robot. And I am happy to create PR for this functionality if it not already been taken care of.

Any interest in supporting tab/windows on same origin with different window hierarchies

Would be great to be able to use the same message bus for syncing across tabs/windows on the same origin without having to provide a window reference.

I know there are a few mechanisms that can be used to implement this: localStorage, SharedWorker, ServiceWorker, WebSocket

Is there any interest? I've implemented this in various forms and would be open to contributing.

Send a message to All child Iframes

I need to send to all xcomponents running within a parent window a certain message (like broadcasting)....

Can post-robot handle this requirement in any way I'm not seeing ?

Add breaking changes in release notes

As I stated in another issue (#71), some breaking changes have been shipped with v10 but are not mentioned anywhere, such as:

  • postRobot.CONFIG not existing anymore (breaking builds if you used to disable logs using postRobot.CONFIG.LOG_LEVEL)
  • .send() now only accepting a window object instead of an iframe, also breaking tests (induced by 675ddec#diff-bf171c73c84cd3085514c56671eb2a5cL1)

It would be great to add a list of breaking changes made between two major releases :)

documentation

Hi,

I could not find API documentation for post-robot. Is there any official source serving as reference? It is great to have the examples, but it would be good to have a proper reference documentation (including types).

For instance, in the demo postRobot.bridge.openBridge is used but I could not find what that refers to in the README.md.

Also for instance, there is an init channel mentioned in the README which can listened on. I could not figure out whether that init event was an event automatically sent when the iframe has loaded.

Then, if one sends a message to a window, and that window for some reason is not ready, or if one sends message to the window faster than it can process, is there a buffer or alternative back-pressure mechanism set up with post-robot? Or is it expected that messages are sent one by one, after receiving some ACK from the receiver?

If one receives a message, and returns a value, is there also an ACK mechanism to signal that the receiver indeed received the returned value?

Is the secure message channel only created when both domain and window are filled in? cf. https://github.com/krakenjs/post-robot#secure-message-channel

Thanks for the good work, this is a great library

Uncaught Error: Target window is closed

I just tried updating from post-robot 9.0.35 to 10.0.1 and communication fails with a "Target window is closed" exception in Chrome 71.
The relevant code section is https://github.com/krakenjs/cross-domain-utils/blob/2f9d27f602d697ace680dc7fadf09e25f0ec8389/src/utils.js#L505. top and parent do not exist on the win variable.

<!DOCTYPE html>
<html>
<head>
	<title>client.html</title>
	<style type="text/css">
		html, body, iframe {
			width: 100%;
			height: 100%;
			padding: 0;
			margin: 0;
			border: none;
		}
	</style>
</head>
<body>
<iframe src="extension.html"></iframe>
<script type="text/javascript" src="https://unpkg.com/[email protected]/dist/post-robot.js"></script>
<script type="text/javascript">
	postRobot.send(document.getElementsByTagName("iframe")[0], "getInfo", null, { domain: "*" }).then(function(event) {
		console.log("Extension sent info", event);
	});
	setTimeout(() => postRobot.send(document.getElementsByTagName("iframe")[0], "data", {}, { domain: "*" }), 500);
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
	<title>extension.html</title>
	<style type="text/css">
		html, body, div, iframe {
			width: 100%;
			height: 100%;
			margin: 0;
			padding: 0;
			border: none;
		}
	</style>
</head>
<body>
	<script type="text/javascript" src="https://unpkg.com/[email protected]/dist/post-robot.js"></script>
	<script type="text/javascript">
		postRobot.on("data", function(event) {
			console.log("received data", event.data);
		});
	</script>
</body>
</html>

Clearer documentation and/or examples

I'm really interested in using post-robot for an internal PayPal project (you can Slack me for details), but I don't find the README to be quite clear enough on how to get started. It looks like post-robot is a singleton, but I'm not sure how to establish private two-way communication between a parent a child window. Maybe I am expecting it to work more like MessageChannel, and I'm going to plunge ahead and try to implement it anyway, but I think a usage guide and a couple more complete examples would really help.

Thanks!

Serializing Date objects?

Does post-robot support sending Date objects? Just tried to return a date object within an object and found that it returns an empty object. It looks like perhaps there is a minimal check if (typeof item == "object") which will be true for Date objects. Should it have a stronger check like item.constructor === Object and throw an error if false?

Importing post-robot

Hi! I'm having a bit of trouble with importing post-robot inside an Ember component. I strongly suspect this is something easy:

import Ember  from 'ember';
import postRobot from 'post-robot';

This will fail with the following message:

Error: Could not find module `post-robot` imported from (...)

How I'm supposed to refer to postRobot from here?? I added it to my project with npm install --save post-robot...

PostRobot won't handle ES6 Maps

PostRobot won't handle ES6 Maps correctly. I have spent quite a bit of time looking into this, then found that my data was being lost by postRobot. This is because it uses JSON.stringify under the hood, which flattens Maps into empty objects (if the Map is used correctly, ie. with setters, see here).

I believe that this should at least be documented in the readme file, if not properly signalled in the console, otherwise people won't even know what's going wrong.

Disable logs in v10

Is there any particular reason CONFIG is not available anymore inside the postRobot object?

postRobot.CONFIG.LOG_LEVEL = 'disabled';

--> Is now making our tests fail

We used it to disable logs completely on production environments because there were some remaining error logs that we were already catching and processing our own way.

Is there a way we can achieve the same behavior in v10?

Unwanted rewriting of stack & name properties from Error serializer

Is there any particular reason .stack and .name Error properties are not serialized as-is ?
Currently .stack is prefixed with .message and .name is simply replaced with the default Error one.

Also, I currently have to use your non-standard .code property to pass error data (typically an object β€” for an xhr error for example) between child/parents. Would it be possible to add a new .data or .whatever property to the error serializer ? This would be particularly useful when extending Error using ES6 classes.

Unregister postRobot listener

I there a way to unregister a specific postRobot listener?
If I am using postRobot in an angular 2 app for example and want to register onto a postMessage, but only if a specific Component is loaded. And I want to unload it if the Component isn't present anymore.

I added the postRobot.on('x') listener but I cannot "destroy" it.

Is there a way to do this?

Thanks.

specify window object to listen to does not work

Basically, I want to set up a one-one pair that when child iframe send a event, the parent page get it.

Code in the parent page:

let iframeInstance = document.getElementById("frameId") 
// or iframeInstance = window[frameName]
then 
let listener = postRobot.listener({ window: iframeInstance});
and call
listener.on('getDefinition', event => {...}

or I directly use

postRobot.on('getDefinition', {window: iframeInstance}, event => {..}

the code inside the iframe

postRobot.send(window.parent, "getDefinition", {}).then(function (event) {

However, if I don't have the {widnow: iframeInstance}, it works, but once I specify the iframe object, when the child iframe send event, I get this in the console.,

Uncaught (in promise) Error: No handler found for post message: getDefinition from http://localhost:3000 in http://localhost:8080/
    at eval (eval at <anonymous> (pseudoShell-bundle.js:4949), <anonymous>:1614:37)
    at Function.value (eval at <anonymous> (pseudoShell-bundle.js:4949), <anonymous>:1399:41)

I tried different properties, like {id: frameId}, it doesn't work for me either

This is really important when you have multiple iframe in one page.

Garbage Collection for sent functions?

Thank you for writing this awesome library.

I have a question though: I understand post-robot lets me send functions.

post-robot takes the function, and persists it locally in the sending window under a unique id

When does post-robot remove a function that has been persisted in this way? AFAIK it has no way of knowing, whether the other window is still referencing the function.

I looked over the code and found nothing that would ever remove once-persisted functions (only when the other window closes, are all persisted functions removed AFAICS).

If post-robot doesn't remove any persisted functions, building an application that sends lots of closures needs to worry about memory bloat. Because all objects reachable from all closures that have ever been sent via post-robot would not be GC'ed. At least not, while the other window remains open.

Or am I missing something?

post-robot vs post-robot-ie?

Request for clarification - the docs don't mention when to use which .js file, but one would assume from the names that the '-ie' variant is specifically for MS IE. However, if I try to run the demo on macOS/Chrome and edit parent/child to use post-robot.js, it doesn't work (no post button appears on the child window, (empty ).

I'll just use the '-ie' version but was curious about when each should be used.

__TEST__ is not defined

Hi,
first of all thanks for open sourcing this. We were in the process of implementing something similar and this saves us a lot of time.

We're using this via NPM in a project compiled with Webpack and we get the following error:

Uncaught ReferenceError: __TEST__ is not defined
    at Object.__webpack_exports__.b (post-robot.js:1414)
    at __webpack_require__ (post-robot.js:12)
    at Object.__webpack_exports__.i (post-robot.js:37)
    at __webpack_require__ (post-robot.js:12)
    at Object.<anonymous> (post-robot.js:570)
    at __webpack_require__ (post-robot.js:12)
    at Object.<anonymous> (post-robot.js:363)
    at __webpack_require__ (post-robot.js:12)
    at Object.<anonymous> (post-robot.js:435)
    at __webpack_require__ (post-robot.js:12)

As soon as we define __TEST__ everything runs smoothly, is that intended?

Supporting subdomains

I'm proposing a feature idea for post-robot to accept messages from top-level domains like postRobot.send(someWindow, "messageType", data, { domain: "*.domain.com" });

Thoughts?

Unbinding or .off() ?

Sorry if this is a silly question or the wrong place to ask it but I was curious, is there a method for unbinding postRobot listeners?

For example:
Previously in my react components I was doing something like...

componentWillMount() {
     window.addEventListener('message', this.onMessage);
}

componentWillUnmount() {
    window.removeEventListener('message', this.onMessage);
}

Now with postRobot I have something like...

componentWillMount() {
    postRobot.on('ON_MESSAGE', { window: window.parent }, (event) => {
      this.onMessage(event);
    });
}

So i was wondering do I need to unbind? I was imagining something like...

componentWillUnmount() {
    postRobot.off('ON_MESSAGE');
}

Or is this automatically handled since each request is hashed as a new postMessage?

facing issue in returning response in case error occurred

My Scenario is like
I am sending request using postRobot.send from client page
Capturing request in postRobot.on server page
Inside postRobot.on calling a function "func xyz()"which returns ajax request/response.

So, In my case postRobot is working fine, ajax response is returning back to postRobot.send
But when any error occurs in ajax call or response is coming as a error, I didn't find any way to return that error response back to postRobot.send.

I tried promises but I am getting error in console like β€˜Can not reject promise with another promise'.

Please help!!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.