Skip to content

feat: react TS and hooks #193

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

Merged
merged 1 commit into from
Aug 30, 2022
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"test-gen-openapi3": "rm -rf ./tmp && yarn build && ENTRYPOINT=https://demo.api-platform.com/docs.json FORMAT=openapi3 ./testgen.sh",
"test-gen-custom": "rm -rf ./tmp && yarn build && babel src/generators/ReactGenerator.js src/generators/BaseGenerator.js -d ./tmp/gens && cp -r ./templates/react ./templates/react-common ./templates/entrypoint.js ./tmp/gens && ./lib/index.js https://demo.api-platform.com ./tmp/react-custom -g \"$(pwd)/tmp/gens/ReactGenerator.js\" -t ./tmp/gens",
"test-gen-env": "rm -rf ./tmp && yarn build && API_PLATFORM_CLIENT_GENERATOR_ENTRYPOINT=https://demo.api-platform.com API_PLATFORM_CLIENT_GENERATOR_OUTPUT=./tmp ./lib/index.js",
"test-react-app": "rm -rf ./tmp/app && mkdir -p ./tmp/app && yarn create react-app ./tmp/app/reactapp && yarn --cwd ./tmp/app/reactapp add react-router-dom@5 redux redux-thunk react-redux redux-form connected-react-router && cp -R ./tmp/react/* ./tmp/app/reactapp/src && cp ./templates/react/index.js ./tmp/app/reactapp/src && start-server-and-test 'BROWSER=none yarn --cwd ./tmp/app/reactapp start' http://127.0.0.1:3000/books/ 'yarn playwright test'",
"test-react-app": "rm -rf ./tmp/app && mkdir -p ./tmp/app && yarn create react-app --template typescript ./tmp/app/reactapp && yarn --cwd ./tmp/app/reactapp add react-router-dom react-hook-form && cp -R ./tmp/react/* ./tmp/app/reactapp/src && cp ./templates/react/index.tsx ./tmp/app/reactapp/src && start-server-and-test 'BROWSER=none yarn --cwd ./tmp/app/reactapp start' http://127.0.0.1:3000/books/ 'yarn playwright test'",
"test-next-app": "rm -rf ./tmp/app && mkdir -p ./tmp/app && yarn create next-app --typescript ./tmp/app/next && yarn --cwd ./tmp/app/next add isomorphic-unfetch formik react-query && cp -R ./tmp/next/* ./tmp/app/next && rm ./tmp/app/next/pages/index.tsx && rm -rf ./tmp/app/next/pages/api && yarn --cwd ./tmp/app/next build && start-server-and-test 'yarn --cwd ./tmp/app/next start' http://127.0.0.1:3000/books/ 'yarn playwright test'",
"test-vue-app": "rm -rf ./tmp/app && mkdir -p ./tmp/app && cd ./tmp/app && npm init -y vue@2 -- --router vue && cd ../.. && yarn --cwd ./tmp/app/vue add vuex@3 vuex-map-fields lodash && cp -R ./tmp/vue/* ./tmp/app/vue/src && cp ./templates/vue/main.js ./tmp/app/vue/src && yarn --cwd ./tmp/app/vue build && start-server-and-test 'yarn --cwd ./tmp/app/vue vite preview --host 127.0.0.1 --port 3000' http://127.0.0.1:3000/books/ 'yarn playwright test'",
"test-nuxt-app": "rm -rf ./tmp/app && mkdir -p ./tmp/app && yarn create nuxt-app --answers \"'{\\\"name\\\":\\\"nuxt\\\",\\\"language\\\":\\\"js\\\",\\\"pm\\\":\\\"yarn\\\",\\\"ui\\\":\\\"vuetify\\\",\\\"features\\\":[],\\\"linter\\\":[],\\\"test\\\":\\\"none\\\",\\\"mode\\\":\\\"spa\\\",\\\"target\\\":\\\"static\\\",\\\"devTools\\\":[],\\\"vcs\\\":\\\"none\\\"}'\" ./tmp/app/nuxt && yarn --cwd ./tmp/app/nuxt add moment lodash vuelidate vuex-map-fields && cp -R ./tmp/nuxt/* ./tmp/app/nuxt && NUXT_TELEMETRY_DISABLED=1 yarn --cwd ./tmp/app/nuxt generate && start-server-and-test 'yarn --cwd ./tmp/app/nuxt start --hostname 127.0.0.1' http://127.0.0.1:3000/books/ 'yarn playwright test'"
Expand Down
176 changes: 95 additions & 81 deletions src/generators/ReactGenerator.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,49 @@
import chalk from "chalk";
import handlebars from "handlebars";
import hbhComparison from "handlebars-helpers/lib/comparison.js";
import BaseGenerator from "./BaseGenerator.js";

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

this.registerTemplates("common/", [
this.registerTemplates("react/", [
// utils
"utils/mercure.js",
]);

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

// utils
"utils/dataAccess.js",

// reducers
"reducers/foo/create.js",
"reducers/foo/delete.js",
"reducers/foo/index.js",
"reducers/foo/list.js",
"reducers/foo/update.js",
"reducers/foo/show.js",
]);
"utils/dataAccess.ts",
"utils/types.ts",

// hooks
"hooks/create.ts",
"hooks/delete.ts",
"hooks/fetch.ts",
"hooks/index.ts",
"hooks/list.ts",
"hooks/mercure.ts",
"hooks/retrieve.ts",
"hooks/update.ts",
"hooks/show.ts",

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

this.registerTemplates(`react/`, [
// components
"components/foo/Create.js",
"components/foo/Form.js",
"components/foo/index.js",
"components/foo/List.js",
"components/foo/Update.js",
"components/foo/Show.js",
"components/foo/Create.tsx",
"components/foo/Form.tsx",
"components/foo/index.ts",
"components/foo/List.tsx",
"components/foo/Update.tsx",
"components/foo/type.ts",
"components/foo/Show.tsx",
"components/Field.tsx",
"components/Links.tsx",
"components/Pagination.tsx",

// routes
"routes/foo.js",
"routes/foo.tsx",
]);

handlebars.registerHelper("compare", hbhComparison.compare);
}

help(resource) {
Expand All @@ -52,20 +54,14 @@ export default class extends BaseGenerator {
resource.title
);
console.log(
"Paste the following definitions in your application configuration (`client/src/index.js` by default):"
"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 routes
import ${titleLc}Routes from './routes/${titleLc}';

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

// Add routes to <Switch>
// Add routes to <Routes>
{ ${titleLc}Routes }
`)
);
Expand All @@ -74,70 +70,87 @@ combineReducers({ ${titleLc},/* ... */ }),
generate(api, resource, dir) {
const lc = resource.title.toLowerCase();
const ucf = this.ucFirst(resource.title);
const fields = this.parseFields(resource);

const context = {
title: resource.title,
name: resource.name,
lc,
uc: resource.title.toUpperCase(),
ucf,
fields: this.parseFields(resource),
formFields: this.buildFields(resource.writableFields),
fields,
formFields: this.buildFields(fields),
hasRelations: fields.some((field) => field.reference || field.embedded),
hasManyRelations: fields.some(
(field) => field.isReferences || field.isEmbeddeds
),
hydraPrefix: this.hydraPrefix,
title: resource.title,
};

// Create directories
// These directories may already exist
[`${dir}/utils`, `${dir}/config`, `${dir}/routes`].forEach((dir) =>
this.createDir(dir, false)
);

[
`${dir}/actions/${lc}`,
`${dir}/utils`,
`${dir}/config`,
`${dir}/interfaces`,
`${dir}/routes`,
`${dir}/components/${lc}`,
`${dir}/reducers/${lc}`,
].forEach((dir) => this.createDir(dir));
`${dir}/hooks`,
].forEach((dir) => this.createDir(dir, false));

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

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

// reducers
"reducers/%s/create.js",
"reducers/%s/delete.js",
"reducers/%s/index.js",
"reducers/%s/list.js",
"reducers/%s/update.js",
"reducers/%s/show.js",
"components/%s/Create.tsx",
"components/%s/Form.tsx",
"components/%s/index.ts",
"components/%s/List.tsx",
"components/%s/Update.tsx",
"components/%s/type.ts",
"components/%s/Show.tsx",

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

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

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

// components
"components/Field.tsx",
"components/Links.tsx",
"components/Pagination.tsx",

// hooks
"hooks/create.ts",
"hooks/delete.ts",
"hooks/fetch.ts",
"hooks/index.ts",
"hooks/list.ts",
"hooks/mercure.ts",
"hooks/retrieve.ts",
"hooks/update.ts",
"hooks/show.ts",

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

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

getDescription(field) {
Expand All @@ -160,6 +173,7 @@ combineReducers({ ${titleLc},/* ... */ }),
...list,
[field.name]: {
...field,
type: this.getType(field),
description: this.getDescription(field),
readonly: false,
isReferences,
Expand All @@ -169,7 +183,7 @@ combineReducers({ ${titleLc},/* ... */ }),
};
}, {});

return fields;
return Object.values(fields);
}

ucFirst(target) {
Expand Down
47 changes: 27 additions & 20 deletions src/generators/ReactGenerator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,33 +37,40 @@ test("Generate a React app", () => {
generator.generate(api, resource, tmpobj.name);

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

"/actions/abc/create.js",
"/actions/abc/delete.js",
"/actions/abc/list.js",
"/actions/abc/show.js",
"/actions/abc/update.js",
"/interfaces/Abc.ts",
"/interfaces/Collection.ts",

"/components/abc/index.js",
"/components/abc/Create.js",
"/components/abc/Update.js",
"/components/abc/index.ts",
"/components/abc/Create.tsx",
"/components/abc/Update.tsx",
"/components/abc/type.ts",

"/routes/abc.js",
"/components/Field.tsx",
"/components/Links.tsx",
"/components/Pagination.tsx",

"/reducers/abc/create.js",
"/reducers/abc/delete.js",
"/reducers/abc/index.js",
"/reducers/abc/list.js",
"/reducers/abc/show.js",
"/reducers/abc/update.js",
"/routes/abc.tsx",

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

[
"/components/abc/Form.js",
"/components/abc/List.js",
"/components/abc/Show.js",
"/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/);
Expand Down
4 changes: 2 additions & 2 deletions templates/next/components/foo/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ export const Form: FunctionComponent<Props> = ({ {{{lc}}} }) => {
...{{lc}},
{{#each fields}}
{{#if isEmbeddeds}}
{{name}}: {{../lc}}.{{name}}?.map((emb: any) => emb['@id']) ?? "",
{{name}}: {{../lc}}["{{name}}"]?.map((emb: any) => emb['@id']) ?? [],
{{else if embedded}}
{{name}}: {{../lc}}.{{name}}?.['@id'] ?? "",
{{name}}: {{../lc}}["{{name}}"]?.['@id'] ?? "",
{{/if}}
{{/each}}
} :
Expand Down
4 changes: 3 additions & 1 deletion templates/next/components/foo/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export const List: FunctionComponent<Props> = ({ {{{lc}}}s }) => (
</th>
{{#each fields}}
<td>
{{#if reference}}
{{#if isReferences}}
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'].map((ref: any) => ({ href: getPath(ref, '/{{{lowercase reference.title}}}s/[id]'), name: ref })) } />
{{else if reference}}
<ReferenceLinks items={ { href: getPath({{{../lc}}}['{{{name}}}'], '/{{{lowercase reference.title}}}s/[id]'), name: {{{../lc}}}['{{{name}}}'] } } />
{{else if isEmbeddeds}}
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'].map((emb: any) => ({ href: getPath(emb['@id'], '/{{{lowercase embedded.title}}}s/[id]'), name: emb['@id'] })) } />
Expand Down
14 changes: 8 additions & 6 deletions templates/next/components/foo/Show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ export const Show: FunctionComponent<Props> = ({ {{{lc}}}, text }) => {
<tr>
<th scope="row">{{name}}</th>
<td>
{{#if reference}}
<ReferenceLinks items={ { href: getPath({{{../lc}}}['{{{name}}}'], '/{{{lowercase reference.title}}}s/[id]'), name: {{{../lc}}}['{{{name}}}'] } } />
{{else if isEmbeddeds}}
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'].map((emb: any) => ({ href: getPath(emb['@id'], '/{{{lowercase embedded.title}}}s/[id]'), name: emb['@id'] })) } />
{{else if embedded}}
<ReferenceLinks items={ { href: getPath({{{../lc}}}['{{{name}}}']['@id'], '/{{{lowercase embedded.title}}}s/[id]'), name: {{{../lc}}}['{{{name}}}']['@id'] } } />
{{#if isReferences}}
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'].map((ref: any) => ({ href: getPath(ref, '/{{{lowercase reference.title}}}s/[id]'), name: ref })) } />
{{else if reference}}
<ReferenceLinks items={ { href: getPath({{{../lc}}}['{{{name}}}'], '/{{{lowercase reference.title}}}s/[id]'), name: {{{../lc}}}['{{{name}}}'] } } />
{{else if isEmbeddeds}}
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'].map((emb: any) => ({ href: getPath(emb['@id'], '/{{{lowercase embedded.title}}}s/[id]'), name: emb['@id'] })) } />
{{else if embedded}}
<ReferenceLinks items={ { href: getPath({{{../lc}}}['{{{name}}}']['@id'], '/{{{lowercase embedded.title}}}s/[id]'), name: {{{../lc}}}['{{{name}}}']['@id'] } } />
{{else if (compare type "==" "Date") }}
{ {{{../lc}}}['{{{name}}}']?.toLocaleString() }
{{else}}
Expand Down
Loading