@creuna/prop-types-csharp
v6.2.0
Published
Tools for generating C# classes from React component propTypes
Downloads
139
Keywords
Readme
PropTypes to class generator
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
- TLDR config
- General concepts
- Typescript
- About generated classes
- Node.js API
- Webpack plugin
- Babel plugin
- eslint plugin
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
}
}