Skip to content

Instantly share code, notes, and snippets.

@JamieMason
Last active December 24, 2019 15:41
Show Gist options
  • Save JamieMason/de42502c918c222a22051f19c383dff4 to your computer and use it in GitHub Desktop.
Save JamieMason/de42502c918c222a22051f19c383dff4 to your computer and use it in GitHub Desktop.
Use JavaScript Proxy to log DOM APIs

Use JavaScript Proxy to log DOM APIs

Find out which DOM APIs your code is using with Proxy and Webpack.

Files

src/components/App.js

import { Component, h } from 'preact';

export class App extends Component {
  render({ title }) {
    return (
      <main style={{ textAlign: 'center' }}>
        <h1>{title}</h1>
        <img alt="" src={`https://i.pravatar.cc/100?u=${name}`} style={{ borderRadius: '100%' }} />
      </main>
    );
  }
}

src/proxy/index.js

const isFunction = value => typeof value === 'function';
const looksLikeConstructor = key => key.search(/^[A-Z]/) !== -1;

export const addLoggingToMethod = (name, obj, key) => {
  const { value: fn } = Object.getOwnPropertyDescriptor(obj, key);
  if (isFunction(fn)) {
    obj[key] = function() {
      const args = Array.from(arguments);
      console.group(`[wrapped] ${name}.${key}(`, ...args, ')');
      const returnValue = fn.call(this, ...args);
      console.log(returnValue);
      console.groupEnd();
      return returnValue;
    };
  }
};

export const addLoggingToMethods = (name, obj) => {
  Object.keys(obj).forEach(key => {
    addLoggingToMethod(name, obj, key);
  });
};

export const getLoggingProxyFor = (name, target) =>
  new Proxy(target, {
    get: function(obj, key) {
      const value = Reflect.get(obj, key);
      if (name === 'window' && key === 'document') {
        return document;
      }
      if (!isFunction(value) || looksLikeConstructor(key)) {
        console.group(`[proxied] ${name}.${key}`);
        console.log(value);
        return value;
      }
      const fn = value;
      return function() {
        const args = Array.from(arguments);
        console.group(`[proxied] ${name}.${key}(`, ...args, ')');
        const returnValue = fn.call(obj, ...args);
        console.log(returnValue);
        console.groupEnd();
        return returnValue;
      };
    },
    set: function(obj, key, value) {
      console.log(`[proxied] ${name}.${key} = ${value}`);
      return Reflect.set(obj, key, value);
    },
    deleteProperty: function(obj, key) {
      console.log(`[proxied] delete ${name}.${key}`);
      return Reflect.deleteProperty(obj, key);
    },
    ownKeys: function(obj, key) {
      console.log(`[proxied] Object.getOwnPropertyNames(${name})`);
      return Reflect.ownKeys(obj, key);
    },
    has: function(obj, key) {
      console.log(`[proxied] ${key} in ${obj}`);
      return Reflect.has(obj, key);
    },
    defineProperty: function(obj, key, descriptor) {
      console.log(`[proxied] Object.defineProperty(${name}, ${key},`, descriptor, ')');
      return Reflect.defineProperty(obj, key, descriptor);
    },
    getOwnPropertyDescriptor: function(obj, key) {
      console.log(`[proxied] Object.getOwnPropertyDescriptor(${name}, ${key})`);
      return Reflect.getOwnPropertyDescriptor(obj, key);
    },
  });

// Add logging to methods of eg Element.prototype
export const register = () => {
  Object.getOwnPropertyNames(window)
    .filter(name => name.startsWith('HTML') || name.endsWith('Element'))
    .concat('Element', 'Event', 'Node', 'Text')
    .forEach(name => {
      if (window[name] && window[name].prototype) {
        addLoggingToMethods(`${name}.prototype`, window[name].prototype);
      }
    });
};

src/proxy/register.js

const { addLoggingToMethod, addLoggingToMethods } = require('.');

// Add logging to methods of eg Element.prototype
Object.getOwnPropertyNames(window)
  .filter(name => name.startsWith('HTML') || name.endsWith('Element'))
  .concat('Element', 'Event', 'Node', 'Text')
  .forEach(name => {
    if (window[name] && window[name].prototype) {
      addLoggingToMethods(`${name}.prototype`, window[name].prototype);
    }
  });

src/index.js

require('./proxy/register');

const { h, Component, render } = require('preact');
const { App } = require('./components/App');
const app = <App title="What DOM does this use?" />;

render(app, document.getElementById('root'));

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>What DOM does this use?</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/dist/index.js"></script>
  </body>
</html>

package.json

{
  "name": "what-dom-does-preact-10-use",
  "version": "0.0.0",
  "dependencies": {
    "preact": "10.1.1"
  },
  "devDependencies": {
    "@babel/cli": "7.7.7",
    "@babel/core": "7.7.7",
    "@babel/plugin-transform-react-jsx": "7.7.7",
    "@babel/preset-env": "7.7.7",
    "babel-loader": "8.0.6",
    "imports-loader": "0.8.0",
    "webpack": "4.41.4",
    "webpack-cli": "3.3.10"
  },
  "private": true,
  "scripts": {
    "build": "rm -rf dist && webpack --config webpack.config.js"
  }
}

src/proxy/document.js

const { getLoggingProxyFor } = require('.');

module.exports = getLoggingProxyFor('document', window.document);

webpack.config.js

const path = require('path');
const webpack = require('webpack');

const srcPath = path.resolve(__dirname, 'src');
const distPath = path.resolve(__dirname, 'dist');

module.exports = {
  context: srcPath,
  devtool: false,
  mode: 'development',
  target: 'web',
  entry: './index.js',
  output: {
    filename: './index.js',
    path: distPath,
  },
  module: {
    rules: [
      {
        test: filePath => !filePath.includes('/proxy/'),
        use: `imports-loader?${[`document=${require.resolve('./src/proxy/document.js')}`].join('&')}`,
      },
      {
        test: /\.m?jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [['@babel/preset-env', { targets: { firefox: '71' } }]],
            plugins: [['@babel/plugin-transform-react-jsx', { pragma: 'h', pragmaFrag: 'Fragment' }]],
          },
        },
      },
    ],
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': JSON.stringify({
        NODE_ENV: process.env.NODE_ENV || 'development',
      }),
    }),
  ],
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment