npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@jamsch/react-mockups

v0.8.1

Published

Tools to develop components in isolation for React & React Native

Downloads

137

Readme

@jamsch/react-mockups

npm version bundle size

react-mockups is a lean (no runtime dependencies) alternative to Storybook that provides a similar API with CLI & IDE tooling for React & React Native to develop your components in isolation. It's primary goal is to be easily integrated in to your web & native projects (as a regular component) without requiring any custom build tools.

preview

Installation

npm install -S @jamsch/react-mockups

CLI

This library makes heavy use of a mockups.js file to load your components. The spec of a mockups.js file is the following:

// mockups.js
import * as MockupName from './path/to/mockup';
export default {
  './path/to/mockup.js': MockupName,
  // etc...
};

This package includes a CLI tool that finds all *.mockup.js/ts/tsx files in your project to generate a mockups.js file. You can configure it as a CLI argument or through package.json

Configuring through package.json

  1. Add a config key with the following options:
// package.json
{
  "scripts": {
    "mockup:generate": "react-mockups generate"
  },
  "config": {
    "react-mockups": {
      "searchDir": ["./src"],
      "pattern": "**/*.mockup.tsx",
      "outputFile": "./src/mockups.ts",
      // (optional) Server options
      "port": "1337",
      "host": "127.0.0.1"
    }
  }
}
  1. Run npm run mockup:generate to generate the mockups.ts file.

CLI arguments

Alternatively, you can call the CLI directly in case you'd like to generate multiple mockup files for various parts of your app.

react-mockups [options]

Commands:
  react-mockups server [-p 1337]  Start the server
  react-mockups generate          Generate the mockups file

Options:
  --version     Show version number                                       [boolean]
  --help        Show help                                                 [boolean]
react-mockups generate

Generate the mockups file

Options:
  --searchDir    The directory or directories, relative to the project root, to
                 search for files in.                                    [array]
  --pattern      Pattern to search the search directories with. Note: if pattern
                 contains '**/*' it must be escaped with quotes         [string]
  --outputFile   Path to the output file.                               [string]
  --startServer  Starts the server                                     [boolean]
  --debug        Sets log level to debug                               [boolean]
  --silent       Silences all logging                                  [boolean]
react-mockups server [-p 1337]

Start the server

Options:
  -p, --port     Port to listen on                      [number] [default: 1337]
  -h, --host     Hostname                        [string] [default: "127.0.0.1"]

Basic Usage

  1. Create a file in your project, ending in *.mockup.jsx/tsx
// Button.mockup.jsx
import React from 'react';
import { Button, View } from 'react-native';

export default {
  // What will be displayed on the root view
  title: 'Buttons',
  // What will be rendered when selected
  component: () => {
    return (
      <View>
        <Button title="Red button" style={{ backgroundColor: 'red' }} />
        <Button title="Blue button" style={{ backgroundColor: 'blue' }} />
      </View>
    );
  },
};
  1. Run the CLI to generate your mockups file (or create one yourself using the spec above)
  2. Create a component that imports MockupRoot and provide it the list of mockups
// MockupApp.jsx
import React from 'react';
import { MockupRoot } from '@jamsch/react-mockups/native';
// for web: import { MockupRoot } from '@jamsch/react-mockups/web';
import mockups from './mockups'; // your generated file

export default function MockupApp() {
  return <MockupRoot mockups={mockups} />;
}
  1. Render the component anywhere in your app.

Tip: For react-native projects you can conditionally load your Mockup view as the app root using babel-plugin-transform-inline-environment-variables, for example:

// babel.config.js
module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: ['transform-inline-environment-variables'],
};
// package.json
"scripts": {
  "start": "react-native start",
  "mockup": "cross-env MOCKUP=true npm start",
  "premockup": "react-mockups generate"
}
// index.js
import App from './src/App';

if (process.env.MOCKUP) {
  const MockupApp = require('./src/MockupApp').default;
  AppRegistry.registerComponent('RnDiffApp', () => MockupApp);
} else {
  AppRegistry.registerComponent('RnDiffApp', () => App);
}

TypeScript usage

You can import Meta to help assist typechecking exports on *.mockup.tsx files

// Button.mockup.tsx
import React from 'react';
import { Button, View } from 'react-native';
import type { Meta } from '@jamsch/react-mockups';

export default {
  // What will be displayed on the root view
  title: 'Buttons',
  // What will be rendered when selected
  component: () => {
    return (
      <View>
        <Button title="Red button" style={{ backgroundColor: 'red' }} />
        <Button title="Blue button" style={{ backgroundColor: 'blue' }} />
      </View>
    );
  },
} as Meta;

Persisting last viewed mockup across refreshes

In case you'd like to persist the same mockup view across refreshes, you'd probably want to use a package like @react-native-async-storage/async-storage to store the path. Here's a quick recipe.

import React, { useState, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { MockupRoot } from '@jamsch/react-mockups/native';
import mockups from './mockups'; // your generated file

const STORAGE_KEY = 'MOCKUP_INITIAL_PATH';

export default function MockupApp() {
  const [initialPath, setInitialPath] = useState(null);

  // Load from AsyncStorage
  useEffect(() => {
    AsyncStorage.getItem(STORAGE_KEY)
      .then((path) => {
        setInitialPath(path || '');
      })
      .catch(() => setInitialPath(''));
  }, []);

  // Wait till we've tried to fetch from AsyncStorage
  if (typeof initialPath !== 'string') {
    return null;
  }

  // Render the view
  return (
    <MockupRoot
      mockups={mockups}
      // When a navigation occurs, store the path in AsyncStorage
      onNavigate={(path) => {
        if (path) {
          AsyncStorage.setItem(STORAGE_KEY, path);
        } else {
          // The user may navigate back to the root
          AsyncStorage.removeItem(STORAGE_KEY);
        }
      }}
      // Initial component to render
      initialPath={initialPath}
    />
  );
}

Configuring Mockup List Item layout

// MockupApp.jsx
import React from 'react';
import { Pressable, Text } from 'react-native';
import { MockupRoot } from '@jamsch/react-mockups/native';
import mockups from './mockups'; // your generated file

export default function MockupApp() {
  return (
    <MockupRoot
      mockups={mockups}
      // Customise the list item to render
      renderItem={({ path, navigate, title }) => {
        return (
          <Pressable
            key={path}
            onPress={() => navigate(path)}
            android_ripple={{ borderless: false }}
          >
            <Text>Mockup: {title}</Text>
          </Pressable>
        );
      }}
    />
  );
}

MockupRoot props

{
 /** Initial path when mounting */
  initialPath?: string;
  /** List of mockups */
  mockups: Record<string, NodeRequire>;
  /** When a navigation occurs */
  onNavigate?: (path: string | null) => void;
  /** Customise item rendering */
  renderItem?: (params: {
    path: string;
    title: string;
    navigate: (path: string) => void;
  }) => React.ReactNode;
  /** Wrapper component to render a mockup component. Tip: call `navigate(null)` to navigate back to the root */
  Wrapper?: FunctionComponent<{
    title: string;
    path: string;
    Component: ComponentType<any>;
    navigate: (path: string | null) => void;
  }>;
  /** Path to websocket server */
  server?: string;
}

Running the preview server

react-mockups includes a tiny Node.js http & websocket server to allow for integration with IDE tooling (VS code extensions) & web apps.

Preview:

mockup-server-preview

  1. Run react-mockups server (or npm run mockups:server as shown below) to start the server
// package.json
{
  "scripts": {
    "mockups:server": "react-mockups server",
    // alternatively, run server & your app using the same command
    "dev": "npm-run-all --parallel mockups:server start"
  }
}
  1. Add server="[host]:[port]" to MockupRoot
import React from 'react';
import { MockupRoot } from '@jamsch/react-mockups/native';
import mockups from './mockups';

export default function App() {
  return (
    <MockupRoot
      mockups={mockups}
      server="localhost:1337" // path to your server
    />
  );
}

Note: for Android devices, you'll likely need to run the following command in order to connect to the websocket server.

adb reverse tcp:1337 tcp:1337

VS Code extension

You can install the following VS Code extension if you'd like to navigate between mockup files in your local codebase and on your app. You'll need to be running the Mockup server (react-mockups server) to use this extension.

  • Repository: https://github.com/jamsch/react-native-mockups-explorer-vscode
  • Visual Studio Marketplace: https://marketplace.visualstudio.com/items?itemName=jamsch.react-native-mockups-explorer-vscode

The websocket server API

If you'd like to create your own IDE tooling, here's a quick starter.

/*
type Mockup = {
  title: string;
  path: string;
  children?: Mockup[];
};
*/

let state = {
  /** The project's root directory. Useful for navigating to individual mockup files */
  projectRoot: "",
  /** Whether the server has synced with any app client */
  hasSynced: false,
  /** {string|null} Current relative path to mockup. You can determine the active mockup by searching "mockups[x].path" */
  path: null,
  /** {Mockup[]} List of synced mockups with the app client */
  mockups: [];
}

function createWebsocket() {
  // Connect to websocket server
  const websocket = new WebSocket('ws://localhost:1337/websocket');

  websocket.onopen = () => {
    // Ping the server to get the initial state
    websocket.send(JSON.stringify({ type: 'PING' }));
  };

  websocket.onclose = () => {
    // Notify user that they've lost connection to the mockup server
    // call reconnect() once confirming with the user
  };

  websocket.onerror = (error) => {
    // Notify user that they've failed to connect to the server
    // call reconnect() once confirming with the user
  };

  websocket.onmessage = (event) => {
    const message = JSON.parse(event.data);
    switch (message.type) {
      // Once an app client has connected, you'll get a "SYNC_STATE" message
      case 'SYNC_STATE': {
        state = message.payload;
        /* {
            type: "SYNC_STATE",
            payload: {
              projectRoot: string;
              hasSynced: boolean,
              path: string;
              mockups: Mockup[];
            }>
          }*/
        break;
      }
      case 'NAVIGATE': {
        // { type: "NAVIGATE", payload: "./src/components/ui/Button.tsx" }
        state.path = message.payload;
        break;
      }
    }
  };

  return websocket;
}

// create a websocket in some context (preferably in a class)
global.websocket = createWebsocket();

const reconnect = () => {
    // Reset event listeners to avoid duplicate messages
    global.websocket.onclose = () => {};
    global.websocket.close();
    global.websocket = createWebsocket();
    // Re-render your UI
    // render()
  }
}

const onButtonPress = (mockup) => {
  // Navigate to a mockup. This will be broadcasted to all clients
  // mockup: { title: 'Button', path: './components/ui/Button.tsx' }
  const message = { type: 'NAVIGATE', payload: mockup.path };
  ws.send(JSON.stringify(message));
}

Contributing

See the contributing guide to learn how to contribute to the repository and the development workflow.

License

MIT