@creuna/prop-types-csharp

Tools for generating C# classes from React component propTypes

Stats

StarsIssuesVersionUpdatedCreatedSize
@creuna/prop-types-csharp
106.2.02 years ago3 years agoMinified + gzip package size for @creuna/prop-types-csharp in KB

Readme

PropTypes to class generator

npm version Travis status Coverage Status

This package has tools for generating classes from React components using propTypes. Curently supports javascript or typescript source and C#, Kotlin or Typescript output.

Table of contents

Install

npm install --save-dev @creuna/prop-types-csharp

TLDR config

Webpack plugin

Config example. See complete list of options below

const PropTypesCSharpPlugin = require('@creuna/prop-types-csharp/webpack-plugin');
const { generators, parsers } = require('@creua/prop-types-csharp');

module.exports = {
  entry: { ... },
  output: { ... },
  plugins: [
    new PropTypesCSharpPlugin({
      exclude: ['node_modules', 'some/path/to/exclude'],
      compilerOptions: {
        parser: parsers.typescript, // Optional
        generator: generators.kotlin // Optional
      }
    })
  ]
};

Babel plugin

Read more below

{
  "plugins": ["@creuna/prop-types-csharp/babel-plugin"]
}

Eslint plugin

Read more below

npm install --save-dev @creuna/eslint-plugin-prop-types-csharp

{
  "plugins": ["@creuna/eslint-plugin-prop-types-csharp"],
  "rules": {
    "@creuna/prop-types-csharp/all": 2
  }
}

General concepts

Ignored props

Props of type func, element, node and instanceOf are ignored when creating classes because they make no sense in server-land.

Unsupported propTypes

Some propTypes are ambiguous and cannot be used for class generation.

PropTypes.object

object should be replaced with shape or a propTypesMeta definition. Using the propTypes of another component is usually the best choice when passing props to child components:

const Component = ({ link }) => <Link {...link} />;

Compontent.propTypes = {
  link: PropTypes.shape(Link.propTypes) // Reference to Link component
};

The above example will result in a class that has a reference to the class for Link, which means the definition of Link is now re-used which is nice:

public class Component {
  public Link Link { get; set; }
}

PropTypes.array

array should be replaced by an arrayOf or have a propTypesMeta definition.

PropTypes.any

Use a different type or a propTypesMeta definition.

PropTypes.symbol

Use a different type or a propTypesMeta definition.

propTypesMeta (String | Object)

String

The only supported string value for propTypesmeta is 'exclude'. When Component.propTypesMeta = 'exclude';, no class will be generated for the component.

Object

In general, it's recommended to define as much as possible in propTypes. In some cases however, that might be difficult, and in those cases propTypesMeta can be helpful.

propTypesMeta can be used to exclude some props from classes or to provide type hints for ambiguous types.

Supported values for props in propTypesMeta are

  • "int"
  • "int?"
  • "float"
  • "float?"
  • "double"
  • "double?"
  • "exclude"
  • React component
  • (< React component > | Object)[]

"int", "float", "double" and their nullable counterparts replace PropTypes.number if supplied. By default, PropTypes.number will result in int in classes.

Functional component:

const Component = () => <div />;

Component.propTypes = {
  someProp: PropTypes.number,
  anotherProp: PropTypes.string,
  someComponent: PropTypes.object,
  items: PropTypes.array,
  numbers: PropTypes.arrayOf(
    PropTypes.shape({
      number: PropTypes.number
    })
  )
};

Component.propTypesMeta = {
  someProp: "float",
  anotherProp: "exclude",
  someComponent: SomeComponent,
  items: [AnotherComponent],
  numbers: [{ number: "float" }]
};

Class component:

class Component extends React.Component {
  static propTypes = {
    someProp: PropTypes.number,
    anotherProp: PropTypes.string,
    someComponent: PropTypes.object,
    items: PropTypes.array,
    numbers: PropTypes.arrayOf(
      PropTypes.shape({
        number: PropTypes.number
      })
    )
  };

  static propTypesMeta = {
    someProp: "float",
    anotherProp: "exclude",
    someComponent: SomeComponent,
    items: [AnotherComponent],
    numbers: [{ number "float" }]
  };
}

Inheritance

Inheritance of propTypes from other components is supported for C#. This will result in C# classes with corresponding inheritance. The baseClass option will be overridden when inheriting.

Simple:

MyComponent.propTypes = OtherComponent.propTypes;

With properties:

// Remember to not mutate other components' propTypes!
MyComponent.propTypes = Object.assign({}, OtherComponent.propTypes, {
  foo: PropTypes.string,
  bar: PropTypes.number
});

Referencing a single property:

MyComponent.propTypes: {
  items: OtherComponent.propTypes.items
};

Typescript as input language

The class generator will determine the name of the generated class based on how prop types are defined:

  • the component name if a type literal is used
  • the name of the interface/type alias if used

If type parameters are used the generator will attempt to use the first parameter as the type definition for the component. Keep in mind that using something other than the prop types as the first argument, class generation might succeed but the generated class will not have the right properties.

Illegal types

As with the javascript parser, some types are not allowed because they are too ambiguous, like object, any, intersection and union types.

const A = (props: { b: string }) => null; // Class name: A
const A: React.FunctionComponent<{ b: string }> = props => null; // Class name: A

type BProps = { c: string };
const B = (props: BProps) => null; // Class name: BProps
const B: React.FunctionComponent<BProps> = props => null; // Class name: BProps

const CProps = { d: string };
const C: SomeType<any, CProps> = props => null; // Error.

PropTypesMeta

propTypesMeta works in mostly the same way as for javascript components, the only notable expection being the lack of support for referencing other components.

Two type aliases are exported that can be used to validate propTypesMeta:

import {
  PropTypesMeta,
  WithPropTypesMeta
} from "@prop-types-csharp/prop-types-meta";

type AProps = { a: string; b: number };
class A extends React.Component<AProps> {
  static propTypesMeta: PropTypesMeta<AProps> = {
    a: "exclude",
    b: "int?"
  };
}

type BProps = { a: string; b: number };
const B: WithPropTypesMeta<BProps> = props => null;
B.propTypesMeta = { a: "exclude", b: "int?" };

WithPropTypesMeta accepts a second type parameter that can be used if stuff like React.FunctionComponent is needed. Usage is quite verbose so adding your own type alias might be useful.

type BProps = { a: string; b: number };
const B: WithPropTypesMeta<BProps, React.FunctionComponent<BProps>> = props =>
  null;

About generated classes (C#)

Enums

Generated enums look like this:

public enum Theme
{
  [EnumMember(Value = "theme-blue")]
  ThemeBlue = 0
}

This allows for the passing of magic strings from C# to React. To get this working with serialization, set the following in the Application_Start:

using Newtonsoft.Json.Converters;

ReactSiteConfiguration.Configuration
  .SetJsonSerializerSettings(new JsonSerializerSettings
  {
      Converters = new List<JsonConverter> { new StringEnumConverter() }
  });

About generated classes (Kotlin)

Since Kotlin doesn't have a namespace keyword, the namespace compiler option is used to prefix package names (both for imports and package definitions).

With the Kotlin generator, components can only extend other components if they have no required props. This also applies to the baseClass option. This is due to the fact that this compiler does static analysis of one react component at a time and therefore doesn't know what arguments to pass the constructors of other classes.

Inheriting the entire propTypes of another component will result in a typealias being created.

Node.js API

The Node API exports an object:

{
  compile: function(sourceCode, options){} // Creates a class string,
  generators: {
    csharp: function(){},
    kotlin: function(){},
    typescript: function(){}
  },
  parsers: {
    javascript: function(){},
    typescript: function(){}
  }
}

compile(sourceCode, options)

Returns

Returns an object containing:

className: String Name of React component (derived from export declaration).

code: String Source code to generate class from.

sourceCode: String

Source code of a React component as string.

options: Object

baseClass: String

Base class that generated classes will extend

generator: Function = generators.csharp

Set output language. Curently, C#, Kotlin and Typescripts are supported out of the box but new ones can be added. A generator is a function that takes propTypes (an object describing the classes to create), className (the name of the react component) and an options object. It is expected to return a string. The easiest way of adding a new language is probably to clone lib/stringify/csharp and work from there. If you do make a generator for another language, please consider submitting a PR!

header: String

A string that is inserted at the top of all generated files. Good for adding generic comments or imports

indent: Number = 2

Number of spaces of indentation in generated class

namespace: String

Namespace to wrap around generated class

parser: Function = javascript parser

What input language to parse. Javascript and typescript parsers are exported from the main library.

Example

const { compile } = require("@creuna/prop-types-csharp");

const { className, code } = compile(sourceCode, {
  indent: 4,
  namespace: "Some.Awesome.Namespace"
});

Typescript example

const { compile, generators, parsers } = require("@creuna/prop-types-csharp");

const { className, code } = compile(sourceCode, {
  parser: parsers.typescript,
  generator: generators.kotlin
});

Webpack plugin

The plugin will extract PropType definitions from .jsx files (configurable) and convert them into .cs class files (also configurable). If the build already has errors when this plugin runs, it aborts immediately.

Options: Object

compilerOptions: Object

Options passed to the compiler, such as input language and formatting choices. Supported options are listed in the Node.js API options

exclude: Array of String | RegExp = ['node-modules']

A file is excluded if its path matches any of the exclude patterns. Default is replaced when setting this.

fileExtension: String

Set the file extension of generated classes. If you use the default csharp generator or one of the other bundled generators you can ignore this option.

log: Boolean = false

If set to true, will output some meta information from the plugin.

match: Array of String | RegExp = [/\.jsx$/]

A file is included if its path matches any of the matching patterns (unless it matches an exclude pattern). Default is replaced when setting this.

path: String

Path relative to output.path to put .cs files.

Webpack dev server

When working with webpack-dev-server, the class files will be written to memory instead of disk by default. If you have generated classes included in source control, it could be a good idea to use Webpack dev server's writeToDisk option.

Babel plugin

Having a bunch of propTypesMeta scattered all around your production code might not be what you want. To solve this issue, a Babel plugin is included which, if enabled, will strip all instances of ComponentName.propTypesMeta or static propTypesMeta when building with Webpack.

IMPORTANT

@creuna/prop-types-csharp/babel-plugin needs to be the first plugin to run on your code. If other plugins have transformed the code first, we can't guarantee that it will work like expected.

.babelrc:

{
  "plugins": ["@creuna/prop-types-csharp/babel-plugin"]
}

Eslint plugin

This package includes an eslint plugin. Because eslint requires all plugins to be published separately to npm with a name startig with eslint-plugin-, we've published the proxy package @creuna/eslint-plugin-prop-types-csharp. The proxy package imports the actual plugin code from this package so that it can be used with eslint.

yarn add @creuna/eslint-plugin-prop-types-csharp

Even though the plugin checks many different things in your source code, the plugin only has one rule: all. The reason for this is that it wouldn't really make sense to have control over individual rules, because breaking any of them would also make the class generation fail, so you'll want to have all checks enabled.

.eslintrc.json:

{
  "plugins": ["@creuna/eslint-plugin-prop-types-csharp"],
  "rules": {
    "@creuna/prop-types-csharp/all": 2
  }
}

If you find any bugs or have a feature request, please open an issue on github!

The npm package download data comes from npm's download counts api and package details come from npms.io.