Skip to content

Commit b1c1c39

Browse files
authored
feat: react TS and hooks (#193)
1 parent d819a69 commit b1c1c39

39 files changed

+1379
-764
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"test-gen-openapi3": "rm -rf ./tmp && yarn build && ENTRYPOINT=https://demo.api-platform.com/docs.json FORMAT=openapi3 ./testgen.sh",
6565
"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",
6666
"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",
67-
"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'",
67+
"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'",
6868
"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'",
6969
"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'",
7070
"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'"

src/generators/ReactGenerator.js

Lines changed: 95 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,49 @@
11
import chalk from "chalk";
2+
import handlebars from "handlebars";
3+
import hbhComparison from "handlebars-helpers/lib/comparison.js";
24
import BaseGenerator from "./BaseGenerator.js";
35

4-
export default class extends BaseGenerator {
6+
export default class ReactGenerator extends BaseGenerator {
57
constructor(params) {
68
super(params);
79

8-
this.registerTemplates("common/", [
10+
this.registerTemplates("react/", [
911
// utils
10-
"utils/mercure.js",
11-
]);
12-
13-
this.registerTemplates("react-common/", [
14-
// actions
15-
"actions/foo/create.js",
16-
"actions/foo/delete.js",
17-
"actions/foo/list.js",
18-
"actions/foo/update.js",
19-
"actions/foo/show.js",
20-
21-
// utils
22-
"utils/dataAccess.js",
23-
24-
// reducers
25-
"reducers/foo/create.js",
26-
"reducers/foo/delete.js",
27-
"reducers/foo/index.js",
28-
"reducers/foo/list.js",
29-
"reducers/foo/update.js",
30-
"reducers/foo/show.js",
31-
]);
12+
"utils/dataAccess.ts",
13+
"utils/types.ts",
14+
15+
// hooks
16+
"hooks/create.ts",
17+
"hooks/delete.ts",
18+
"hooks/fetch.ts",
19+
"hooks/index.ts",
20+
"hooks/list.ts",
21+
"hooks/mercure.ts",
22+
"hooks/retrieve.ts",
23+
"hooks/update.ts",
24+
"hooks/show.ts",
25+
26+
// interfaces
27+
"interfaces/Collection.ts",
28+
"interfaces/foo.ts",
3229

33-
this.registerTemplates(`react/`, [
3430
// components
35-
"components/foo/Create.js",
36-
"components/foo/Form.js",
37-
"components/foo/index.js",
38-
"components/foo/List.js",
39-
"components/foo/Update.js",
40-
"components/foo/Show.js",
31+
"components/foo/Create.tsx",
32+
"components/foo/Form.tsx",
33+
"components/foo/index.ts",
34+
"components/foo/List.tsx",
35+
"components/foo/Update.tsx",
36+
"components/foo/type.ts",
37+
"components/foo/Show.tsx",
38+
"components/Field.tsx",
39+
"components/Links.tsx",
40+
"components/Pagination.tsx",
4141

4242
// routes
43-
"routes/foo.js",
43+
"routes/foo.tsx",
4444
]);
45+
46+
handlebars.registerHelper("compare", hbhComparison.compare);
4547
}
4648

4749
help(resource) {
@@ -52,20 +54,14 @@ export default class extends BaseGenerator {
5254
resource.title
5355
);
5456
console.log(
55-
"Paste the following definitions in your application configuration (`client/src/index.js` by default):"
57+
"Paste the following definitions in your application configuration (`client/src/index.tsx` by default):"
5658
);
5759
console.log(
5860
chalk.green(`
59-
// import reducers
60-
import ${titleLc} from './reducers/${titleLc}/';
61-
62-
//import routes
61+
// import routes
6362
import ${titleLc}Routes from './routes/${titleLc}';
6463
65-
// Add the reducer
66-
combineReducers({ ${titleLc},/* ... */ }),
67-
68-
// Add routes to <Switch>
64+
// Add routes to <Routes>
6965
{ ${titleLc}Routes }
7066
`)
7167
);
@@ -74,70 +70,87 @@ combineReducers({ ${titleLc},/* ... */ }),
7470
generate(api, resource, dir) {
7571
const lc = resource.title.toLowerCase();
7672
const ucf = this.ucFirst(resource.title);
73+
const fields = this.parseFields(resource);
7774

7875
const context = {
79-
title: resource.title,
8076
name: resource.name,
8177
lc,
8278
uc: resource.title.toUpperCase(),
8379
ucf,
84-
fields: this.parseFields(resource),
85-
formFields: this.buildFields(resource.writableFields),
80+
fields,
81+
formFields: this.buildFields(fields),
82+
hasRelations: fields.some((field) => field.reference || field.embedded),
83+
hasManyRelations: fields.some(
84+
(field) => field.isReferences || field.isEmbeddeds
85+
),
8686
hydraPrefix: this.hydraPrefix,
87+
title: resource.title,
8788
};
8889

8990
// Create directories
9091
// These directories may already exist
91-
[`${dir}/utils`, `${dir}/config`, `${dir}/routes`].forEach((dir) =>
92-
this.createDir(dir, false)
93-
);
94-
9592
[
96-
`${dir}/actions/${lc}`,
93+
`${dir}/utils`,
94+
`${dir}/config`,
95+
`${dir}/interfaces`,
96+
`${dir}/routes`,
9797
`${dir}/components/${lc}`,
98-
`${dir}/reducers/${lc}`,
99-
].forEach((dir) => this.createDir(dir));
98+
`${dir}/hooks`,
99+
].forEach((dir) => this.createDir(dir, false));
100100

101101
[
102-
// actions
103-
"actions/%s/create.js",
104-
"actions/%s/delete.js",
105-
"actions/%s/list.js",
106-
"actions/%s/update.js",
107-
"actions/%s/show.js",
108-
109102
// components
110-
"components/%s/Create.js",
111-
"components/%s/Form.js",
112-
"components/%s/index.js",
113-
"components/%s/List.js",
114-
"components/%s/Update.js",
115-
"components/%s/Show.js",
116-
117-
// reducers
118-
"reducers/%s/create.js",
119-
"reducers/%s/delete.js",
120-
"reducers/%s/index.js",
121-
"reducers/%s/list.js",
122-
"reducers/%s/update.js",
123-
"reducers/%s/show.js",
103+
"components/%s/Create.tsx",
104+
"components/%s/Form.tsx",
105+
"components/%s/index.ts",
106+
"components/%s/List.tsx",
107+
"components/%s/Update.tsx",
108+
"components/%s/type.ts",
109+
"components/%s/Show.tsx",
124110

125111
// routes
126-
"routes/%s.js",
112+
"routes/%s.tsx",
127113
].forEach((pattern) =>
128114
this.createFileFromPattern(pattern, dir, lc, context)
129115
);
130116

131-
// utils
117+
// interface pattern should be camel cased
132118
this.createFile(
133-
"utils/dataAccess.js",
134-
`${dir}/utils/dataAccess.js`,
135-
context,
136-
false
119+
"interfaces/foo.ts",
120+
`${dir}/interfaces/${context.ucf}.ts`,
121+
context
122+
);
123+
124+
// copy with regular name
125+
[
126+
// interfaces
127+
"interfaces/Collection.ts",
128+
129+
// components
130+
"components/Field.tsx",
131+
"components/Links.tsx",
132+
"components/Pagination.tsx",
133+
134+
// hooks
135+
"hooks/create.ts",
136+
"hooks/delete.ts",
137+
"hooks/fetch.ts",
138+
"hooks/index.ts",
139+
"hooks/list.ts",
140+
"hooks/mercure.ts",
141+
"hooks/retrieve.ts",
142+
"hooks/update.ts",
143+
"hooks/show.ts",
144+
145+
// utils
146+
"utils/dataAccess.ts",
147+
"utils/types.ts",
148+
].forEach((file) =>
149+
this.createFile(file, `${dir}/${file}`, context, false)
137150
);
138-
this.createFile("utils/mercure.js", `${dir}/utils/mercure.js`);
139151

140-
this.createEntrypoint(api.entrypoint, `${dir}/config/entrypoint.js`);
152+
// API config
153+
this.createEntrypoint(api.entrypoint, `${dir}/config/entrypoint.ts`);
141154
}
142155

143156
getDescription(field) {
@@ -160,6 +173,7 @@ combineReducers({ ${titleLc},/* ... */ }),
160173
...list,
161174
[field.name]: {
162175
...field,
176+
type: this.getType(field),
163177
description: this.getDescription(field),
164178
readonly: false,
165179
isReferences,
@@ -169,7 +183,7 @@ combineReducers({ ${titleLc},/* ... */ }),
169183
};
170184
}, {});
171185

172-
return fields;
186+
return Object.values(fields);
173187
}
174188

175189
ucFirst(target) {

src/generators/ReactGenerator.test.js

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,33 +37,40 @@ test("Generate a React app", () => {
3737
generator.generate(api, resource, tmpobj.name);
3838

3939
[
40-
"/utils/dataAccess.js",
41-
"/config/entrypoint.js",
40+
"/utils/dataAccess.ts",
41+
"/utils/types.ts",
42+
"/config/entrypoint.ts",
4243

43-
"/actions/abc/create.js",
44-
"/actions/abc/delete.js",
45-
"/actions/abc/list.js",
46-
"/actions/abc/show.js",
47-
"/actions/abc/update.js",
44+
"/interfaces/Abc.ts",
45+
"/interfaces/Collection.ts",
4846

49-
"/components/abc/index.js",
50-
"/components/abc/Create.js",
51-
"/components/abc/Update.js",
47+
"/components/abc/index.ts",
48+
"/components/abc/Create.tsx",
49+
"/components/abc/Update.tsx",
50+
"/components/abc/type.ts",
5251

53-
"/routes/abc.js",
52+
"/components/Field.tsx",
53+
"/components/Links.tsx",
54+
"/components/Pagination.tsx",
5455

55-
"/reducers/abc/create.js",
56-
"/reducers/abc/delete.js",
57-
"/reducers/abc/index.js",
58-
"/reducers/abc/list.js",
59-
"/reducers/abc/show.js",
60-
"/reducers/abc/update.js",
56+
"/routes/abc.tsx",
57+
58+
"/hooks/create.ts",
59+
"/hooks/delete.ts",
60+
"/hooks/fetch.ts",
61+
"/hooks/index.ts",
62+
"/hooks/list.ts",
63+
"/hooks/mercure.ts",
64+
"/hooks/retrieve.ts",
65+
"/hooks/show.ts",
66+
"/hooks/update.ts",
6167
].forEach((file) => expect(fs.existsSync(tmpobj.name + file)).toBe(true));
6268

6369
[
64-
"/components/abc/Form.js",
65-
"/components/abc/List.js",
66-
"/components/abc/Show.js",
70+
"/components/abc/Form.tsx",
71+
"/components/abc/List.tsx",
72+
"/components/abc/Show.tsx",
73+
"/interfaces/Abc.ts",
6774
].forEach((file) => {
6875
expect(fs.existsSync(tmpobj.name + file)).toBe(true);
6976
expect(fs.readFileSync(tmpobj.name + file, "utf8")).toMatch(/bar/);

templates/next/components/foo/Form.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ export const Form: FunctionComponent<Props> = ({ {{{lc}}} }) => {
5959
...{{lc}},
6060
{{#each fields}}
6161
{{#if isEmbeddeds}}
62-
{{name}}: {{../lc}}.{{name}}?.map((emb: any) => emb['@id']) ?? "",
62+
{{name}}: {{../lc}}["{{name}}"]?.map((emb: any) => emb['@id']) ?? [],
6363
{{else if embedded}}
64-
{{name}}: {{../lc}}.{{name}}?.['@id'] ?? "",
64+
{{name}}: {{../lc}}["{{name}}"]?.['@id'] ?? "",
6565
{{/if}}
6666
{{/each}}
6767
} :

templates/next/components/foo/List.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ export const List: FunctionComponent<Props> = ({ {{{lc}}}s }) => (
3434
</th>
3535
{{#each fields}}
3636
<td>
37-
{{#if reference}}
37+
{{#if isReferences}}
38+
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'].map((ref: any) => ({ href: getPath(ref, '/{{{lowercase reference.title}}}s/[id]'), name: ref })) } />
39+
{{else if reference}}
3840
<ReferenceLinks items={ { href: getPath({{{../lc}}}['{{{name}}}'], '/{{{lowercase reference.title}}}s/[id]'), name: {{{../lc}}}['{{{name}}}'] } } />
3941
{{else if isEmbeddeds}}
4042
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'].map((emb: any) => ({ href: getPath(emb['@id'], '/{{{lowercase embedded.title}}}s/[id]'), name: emb['@id'] })) } />

templates/next/components/foo/Show.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,14 @@ export const Show: FunctionComponent<Props> = ({ {{{lc}}}, text }) => {
4848
<tr>
4949
<th scope="row">{{name}}</th>
5050
<td>
51-
{{#if reference}}
52-
<ReferenceLinks items={ { href: getPath({{{../lc}}}['{{{name}}}'], '/{{{lowercase reference.title}}}s/[id]'), name: {{{../lc}}}['{{{name}}}'] } } />
53-
{{else if isEmbeddeds}}
54-
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'].map((emb: any) => ({ href: getPath(emb['@id'], '/{{{lowercase embedded.title}}}s/[id]'), name: emb['@id'] })) } />
55-
{{else if embedded}}
56-
<ReferenceLinks items={ { href: getPath({{{../lc}}}['{{{name}}}']['@id'], '/{{{lowercase embedded.title}}}s/[id]'), name: {{{../lc}}}['{{{name}}}']['@id'] } } />
51+
{{#if isReferences}}
52+
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'].map((ref: any) => ({ href: getPath(ref, '/{{{lowercase reference.title}}}s/[id]'), name: ref })) } />
53+
{{else if reference}}
54+
<ReferenceLinks items={ { href: getPath({{{../lc}}}['{{{name}}}'], '/{{{lowercase reference.title}}}s/[id]'), name: {{{../lc}}}['{{{name}}}'] } } />
55+
{{else if isEmbeddeds}}
56+
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'].map((emb: any) => ({ href: getPath(emb['@id'], '/{{{lowercase embedded.title}}}s/[id]'), name: emb['@id'] })) } />
57+
{{else if embedded}}
58+
<ReferenceLinks items={ { href: getPath({{{../lc}}}['{{{name}}}']['@id'], '/{{{lowercase embedded.title}}}s/[id]'), name: {{{../lc}}}['{{{name}}}']['@id'] } } />
5759
{{else if (compare type "==" "Date") }}
5860
{ {{{../lc}}}['{{{name}}}']?.toLocaleString() }
5961
{{else}}

0 commit comments

Comments
 (0)