Skip to content

Templates for typescript react application #192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/generators.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import AdminOnRestGenerator from "./generators/AdminOnRestGenerator";
import NextGenerator from "./generators/NextGenerator";
import ReactGenerator from "./generators/ReactGenerator";
import ReactNativeGenerator from "./generators/ReactNativeGenerator";
import ReactTypescriptGenerator from "./generators/ReactTypescriptGenerator";
import TypescriptInterfaceGenerator from "./generators/TypescriptInterfaceGenerator";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interfaces generator is useful in certain cases, possible to keep it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, of course. I'll make changes soon

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check it out 👍

import VueGenerator from "./generators/VueGenerator";
import VuetifyGenerator from "./generators/VuetifyGenerator";
Expand All @@ -24,6 +25,8 @@ export default function generators(generator = "react") {
return wrap(ReactNativeGenerator);
case "typescript":
return wrap(TypescriptInterfaceGenerator);
case "react-typescript":
return wrap(ReactTypescriptGenerator);
case "vue":
return wrap(VueGenerator);
case "vuetify":
Expand Down
217 changes: 217 additions & 0 deletions src/generators/ReactTypescriptGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import chalk from "chalk";
import BaseGenerator from "./BaseGenerator";

export default class ReactTypescriptGenerator extends BaseGenerator {
constructor(params) {
super(params);

this.registerTemplates("react-typescript/", [
// actions
"actions/foo/create.ts",
"actions/foo/delete.ts",
"actions/foo/list.ts",
"actions/foo/update.ts",
"actions/foo/show.ts",

// utils
"utils/dataAccess.ts",
"utils/types.ts",

// reducers
"reducers/foo/create.ts",
"reducers/foo/delete.ts",
"reducers/foo/index.ts",
"reducers/foo/list.ts",
"reducers/foo/update.ts",
"reducers/foo/show.ts",

// types
"types/foo/create.ts",
"types/foo/delete.ts",
"types/foo/list.ts",
"types/foo/show.ts",
"types/foo/update.ts",

// interfaces
"interfaces/Collection.ts",
"interfaces/foo.ts",

// components
"components/foo/Create.tsx",
"components/foo/Form.tsx",
"components/foo/index.tsx",
"components/foo/List.tsx",
"components/foo/Update.tsx",
"components/foo/Show.tsx",

// routes
"routes/foo.tsx"
]);
}

help(resource) {
const titleLc = resource.title.toLowerCase();

console.log(
'Code for the "%s" resource type has been generated!',
resource.title
);
console.log(
"Paste the following definitions in your application configuration (`client/src/index.tsx` by default):"
);
console.log(
chalk.green(`
// import reducers
import ${titleLc} from './reducers/${titleLc}/';

//import routes
import ${titleLc}Routes from './routes/${titleLc}';

// Add the reducer
combineReducers({ ${titleLc},/* ... */ }),

// Add routes to <Switch>
{ ${titleLc}Routes }
`)
);
}

generate(api, resource, dir) {
const lc = resource.title.toLowerCase();
const titleUcFirst = this.ucFirst(resource.title);
const { fields, imports } = this.parseFields(resource);

const context = {
name: resource.name,
lc,
uc: resource.title.toUpperCase(),
ucf: titleUcFirst,
titleUcFirst,
fields,
formFields: this.buildFields(fields),
imports,
hydraPrefix: this.hydraPrefix,
title: resource.title
};

// Create directories
// These directories may already exist
[
`${dir}/utils`,
`${dir}/config`,
`${dir}/interfaces`,
`${dir}/routes`,
`${dir}/actions/${lc}`,
`${dir}/types/${lc}`,
`${dir}/components/${lc}`,
`${dir}/reducers/${lc}`
].forEach(dir => this.createDir(dir));

[
// actions
"actions/%s/create.ts",
"actions/%s/delete.ts",
"actions/%s/list.ts",
"actions/%s/update.ts",
"actions/%s/show.ts",

// components
"components/%s/Create.tsx",
"components/%s/Form.tsx",
"components/%s/index.tsx",
"components/%s/List.tsx",
"components/%s/Update.tsx",
"components/%s/Show.tsx",

// reducers
"reducers/%s/create.ts",
"reducers/%s/delete.ts",
"reducers/%s/index.ts",
"reducers/%s/list.ts",
"reducers/%s/update.ts",
"reducers/%s/show.ts",

// types
"types/%s/create.ts",
"types/%s/delete.ts",
"types/%s/list.ts",
"types/%s/show.ts",
"types/%s/update.ts",

// routes
"routes/%s.tsx"
].forEach(pattern => this.createFileFromPattern(pattern, dir, lc, context));

// interface pattern should be camel cased
this.createFile(
"interfaces/foo.ts",
`${dir}/interfaces/${context.ucf}.ts`,
context
);

// copy with regular name
[
// interfaces
"interfaces/Collection.ts",

// utils
"utils/dataAccess.ts",
"utils/types.ts"
].forEach(file => this.createFile(file, `${dir}/${file}`, context, false));

// API config
this.createEntrypoint(api.entrypoint, `${dir}/config/entrypoint.ts`);
}

getDescription(field) {
return field.description ? field.description.replace(/"/g, "'") : "";
}

parseFields(resource) {
const fields = [
...resource.writableFields,
...resource.readableFields
].reduce((list, field) => {
if (list[field.name]) {
return list;
}

return {
...list,
[field.name]: {
notrequired: !field.required,
name: field.name,
type: this.getType(field),
description: this.getDescription(field),
readonly: false,
reference: field.reference
}
};
}, {});

// Parse fields to add relevant imports, required for Typescript
const fieldsArray = Object.values(fields);
const imports = Object.values(fields).reduce(
(list, { reference, type }) => {
if (!reference) {
return list;
}

return {
...list,
[type]: {
type,
file: `./${type}`
}
};
},
{}
);

return { fields: fieldsArray, imports: Object.values(imports) };
}

ucFirst(target) {
return target.charAt(0).toUpperCase() + target.slice(1);
}
}
80 changes: 80 additions & 0 deletions src/generators/ReactTypescriptGenerator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Api, Resource, Field } from "@api-platform/api-doc-parser/lib";
import fs from "fs";
import tmp from "tmp";
import ReactTypescriptGenerator from "./ReactTypescriptGenerator";

test("Generate a Typescript React app", () => {
const generator = new ReactTypescriptGenerator({
hydraPrefix: "hydra:",
templateDirectory: `${__dirname}/../../templates`
});
const tmpobj = tmp.dirSync({ unsafeCleanup: true });

const fields = [
new Field("bar", {
id: "http://schema.org/url",
range: "http://www.w3.org/2001/XMLSchema#string",
reference: null,
required: true,
description: "An URL"
})
];
const resource = new Resource("abc", "http://example.com/foos", {
id: "abc",
title: "abc",
readableFields: fields,
writableFields: fields
});
const api = new Api("http://example.com", {
entrypoint: "http://example.com:8080",
title: "My API",
resources: [resource]
});
generator.generate(api, resource, tmpobj.name);

[
"/utils/dataAccess.ts",
"/utils/types.ts",
"/config/entrypoint.ts",

"/interfaces/Abc.ts",
"/interfaces/Collection.ts",

"/actions/abc/create.ts",
"/actions/abc/delete.ts",
"/actions/abc/list.ts",
"/actions/abc/show.ts",
"/actions/abc/update.ts",

"/types/abc/create.ts",
"/types/abc/delete.ts",
"/types/abc/list.ts",
"/types/abc/show.ts",
"/types/abc/update.ts",

"/components/abc/index.tsx",
"/components/abc/Create.tsx",
"/components/abc/Update.tsx",

"/routes/abc.tsx",

"/reducers/abc/create.ts",
"/reducers/abc/delete.ts",
"/reducers/abc/index.ts",
"/reducers/abc/list.ts",
"/reducers/abc/show.ts",
"/reducers/abc/update.ts"
].forEach(file => expect(fs.existsSync(tmpobj.name + file)).toBe(true));

[
"/components/abc/Form.tsx",
"/components/abc/List.tsx",
"/components/abc/Show.tsx",
"/interfaces/Abc.ts"
].forEach(file => {
expect(fs.existsSync(tmpobj.name + file)).toBe(true);
expect(fs.readFileSync(tmpobj.name + file, "utf8")).toMatch(/bar/);
});

tmpobj.removeCallback();
});
55 changes: 55 additions & 0 deletions templates/react-typescript/actions/foo/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { SubmissionError } from 'redux-form';
import { fetchApi } from '../../utils/dataAccess';
import { TError, TDispatch } from '../../utils/types';
import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}';
import {
{{{uc}}}_CREATE_ERROR,
{{{uc}}}_CREATE_LOADING,
{{{uc}}}_CREATE_SUCCESS,
IActionError,
IActionLoading,
IActionSuccess
} from '../../types/{{{lc}}}/create';

export function error(error: TError): IActionError {
return { type: {{{uc}}}_CREATE_ERROR, error };
}

export function loading(loading: boolean): IActionLoading {
return { type: {{{uc}}}_CREATE_LOADING, loading };
}

export function success(created: I{{{ucf}}} | null): IActionSuccess {
return { type: {{{uc}}}_CREATE_SUCCESS, created };
}

export function create(values: Partial<I{{{ucf}}}>) {
return (dispatch: TDispatch) => {
dispatch(loading(true));

return fetchApi('{{{name}}}', { method: 'POST', body: JSON.stringify(values) })
.then(response => {
dispatch(loading(false));

return response.json();
})
.then(retrieved => dispatch(success(retrieved)))
.catch(e => {
dispatch(loading(false));

if (e instanceof SubmissionError) {
dispatch(error(e.errors._error));
throw e;
}

dispatch(error(e.message));
});
};
}

export function reset() {
return (dispatch: TDispatch) => {
dispatch(loading(false));
dispatch(error(null));
};
}
Loading