Skip to content

Commit 7db4c6a

Browse files
Gregcop1dunglas
authored andcommitted
feat: add next.js generator (#130)
* feat: add next generator * fix: @dunglas review * feat: add option to specify serverPath to write route to server configuration * fix: @toofff review
1 parent 4b0e135 commit 7db4c6a

File tree

18 files changed

+756
-47
lines changed

18 files changed

+756
-47
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"handlebars-helpers": "^0.10.0",
4242
"isomorphic-fetch": "^2.2.1",
4343
"mkdirp": "^0.5.1",
44+
"recast": "^0.18.1",
4445
"sprintf-js": "^1.1.1"
4546
},
4647
"scripts": {

src/generators.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import AdminOnRestGenerator from "./generators/AdminOnRestGenerator";
2+
import NextGenerator from "./generators/NextGenerator";
23
import ReactGenerator from "./generators/ReactGenerator";
34
import ReactNativeGenerator from "./generators/ReactNativeGenerator";
45
import TypescriptInterfaceGenerator from "./generators/TypescriptInterfaceGenerator";
@@ -14,6 +15,8 @@ export default function generators(generator = "react") {
1415
switch (generator) {
1516
case "admin-on-rest":
1617
return wrap(AdminOnRestGenerator);
18+
case "next":
19+
return wrap(NextGenerator);
1720
case "react":
1821
return wrap(ReactGenerator);
1922
case "react-native":

src/generators/BaseGenerator.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import chalk from "chalk";
12
import fs from "fs";
23
import handlebars from "handlebars";
34
import mkdirp from "mkdirp";
@@ -30,7 +31,9 @@ export default class {
3031
return;
3132
}
3233

33-
if (warn) console.log(`The directory "${dir}" already exists`);
34+
if (warn) {
35+
console.log(chalk.yellow(`The directory "${dir}" already exists`));
36+
}
3437
}
3538

3639
createFileFromPattern(pattern, dir, lc, context) {
@@ -55,6 +58,34 @@ export default class {
5558
this.createFile("entrypoint.js", dest, { entrypoint }, false);
5659
}
5760

61+
// eslint-disable-next-line no-unused-vars
62+
checkDependencies(dir) {}
63+
64+
getTargetDependencies(dir) {
65+
const packageFilePath = `${dir}/package.json`;
66+
let packageFile;
67+
let dependencies = [];
68+
try {
69+
if (!fs.existsSync(packageFilePath)) {
70+
throw new Error();
71+
}
72+
packageFile = fs.readFileSync(packageFilePath);
73+
const configuration = JSON.parse(packageFile.toString());
74+
dependencies = Object.keys({
75+
...configuration.dependencies,
76+
...configuration.devDependencies
77+
});
78+
} catch (e) {
79+
console.log(
80+
chalk.yellow(
81+
"There's no readable package file in the target directory. Generator can't check dependencies."
82+
)
83+
);
84+
}
85+
86+
return dependencies;
87+
}
88+
5889
getHtmlInputTypeFromField(field) {
5990
switch (field.id) {
6091
case "http://schema.org/email":
@@ -88,6 +119,28 @@ export default class {
88119
}
89120
}
90121

122+
getType(field) {
123+
if (field.reference) {
124+
return field.reference.title;
125+
}
126+
127+
switch (field.range) {
128+
case "http://www.w3.org/2001/XMLSchema#integer":
129+
case "http://www.w3.org/2001/XMLSchema#decimal":
130+
return "number";
131+
case "http://www.w3.org/2001/XMLSchema#boolean":
132+
return "boolean";
133+
case "http://www.w3.org/2001/XMLSchema#date":
134+
case "http://www.w3.org/2001/XMLSchema#dateTime":
135+
case "http://www.w3.org/2001/XMLSchema#time":
136+
return "Date";
137+
case "http://www.w3.org/2001/XMLSchema#string":
138+
return "string";
139+
}
140+
141+
return "any";
142+
}
143+
91144
buildFields(fields) {
92145
return fields.map(field => ({
93146
...field,

src/generators/NextGenerator.js

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import chalk from "chalk";
2+
import fs from "fs";
3+
import BaseGenerator from "./BaseGenerator";
4+
import { parse, print, types } from "recast";
5+
6+
export default class NextGenerator extends BaseGenerator {
7+
constructor(params) {
8+
super(params);
9+
10+
this.routeAddedtoServer = false;
11+
this.registerTemplates(`next/`, [
12+
// components
13+
"components/common/ReferenceLinks.tsx",
14+
"components/foo/List.tsx",
15+
"components/foo/ListItem.tsx",
16+
"components/foo/Show.tsx",
17+
18+
// interfaces
19+
"error/SubmissionError.ts",
20+
21+
// interfaces
22+
"interfaces/Collection.ts",
23+
"interfaces/foo.ts",
24+
25+
// pages
26+
"pages/foo.tsx",
27+
"pages/foos.tsx",
28+
29+
// utils
30+
"utils/dataAccess.ts"
31+
]);
32+
}
33+
34+
checkDependencies(dir, serverPath) {
35+
const dependencies = this.getTargetDependencies(dir);
36+
37+
if (!dependencies.length) {
38+
return;
39+
}
40+
41+
if (!dependencies.includes("@zeit/next-typescript")) {
42+
console.log(
43+
chalk.yellow(
44+
"It seems next-typescript is not installed but generator needs typescript to work efficiently."
45+
)
46+
);
47+
}
48+
49+
if (!dependencies.includes("express")) {
50+
console.log(
51+
chalk.yellow(
52+
"It seems express is not installed but generator needs a custom express server to work efficiently."
53+
)
54+
);
55+
}
56+
57+
if (serverPath) {
58+
if (!fs.existsSync(serverPath)) {
59+
console.log(chalk.red("Express server file doesn't exists."));
60+
return;
61+
}
62+
63+
const { mode } = fs.statSync(serverPath);
64+
if ("200" !== (mode & parseInt("200", 8)).toString(8)) {
65+
console.log(chalk.red("Express server file is not writable."));
66+
}
67+
}
68+
}
69+
70+
checkImports(directory, imports, extension = ".ts") {
71+
imports.forEach(({ file }) => {
72+
if (fs.existsSync(directory + file + extension)) {
73+
return;
74+
}
75+
76+
console.log(
77+
chalk.yellow(
78+
'An import for the file "%s" has been generated but the file doesn\'t exists.'
79+
),
80+
file
81+
);
82+
});
83+
}
84+
85+
help(resource, dir) {
86+
console.log(
87+
chalk.green('Code for the "%s" resource type has been generated!'),
88+
resource.title
89+
);
90+
91+
// missing import
92+
const { imports } = this.parseFields(resource);
93+
this.checkImports(`${dir}/interfaces/`, imports);
94+
95+
// server route configuration
96+
if (!this.routeAddedtoServer) {
97+
const lc = resource.title.toLowerCase();
98+
console.log(
99+
"Paste the following route to your server configuration file:"
100+
);
101+
console.log(chalk.green(this.getShowRoute(lc)));
102+
}
103+
}
104+
105+
generate(api, resource, dir, serverPath) {
106+
const lc = resource.title.toLowerCase();
107+
const ucf = this.ucFirst(resource.title);
108+
const { fields, imports } = this.parseFields(resource);
109+
110+
const context = {
111+
name: resource.name,
112+
lc,
113+
uc: resource.title.toUpperCase(),
114+
ucf,
115+
fields,
116+
formFields: this.buildFields(resource.writableFields),
117+
imports,
118+
hydraPrefix: this.hydraPrefix,
119+
title: resource.title
120+
};
121+
122+
// Create directories
123+
// These directories may already exist
124+
[
125+
`${dir}/components/common`,
126+
`${dir}/config`,
127+
`${dir}/error`,
128+
`${dir}/interfaces`,
129+
`${dir}/pages`,
130+
`${dir}/utils`
131+
].forEach(dir => this.createDir(dir, false));
132+
133+
// copy with patterned name
134+
this.createDir(`${dir}/components/${context.lc}`);
135+
[
136+
// components
137+
"components/%s/List.tsx",
138+
"components/%s/ListItem.tsx",
139+
"components/%s/Show.tsx",
140+
141+
// pages
142+
"pages/%s.tsx",
143+
"pages/%ss.tsx"
144+
].forEach(pattern =>
145+
this.createFileFromPattern(pattern, dir, context.lc, context)
146+
);
147+
148+
// interface pattern should be camel cased
149+
this.createFile(
150+
"interfaces/foo.ts",
151+
`${dir}/interfaces/${context.ucf}.ts`,
152+
context
153+
);
154+
155+
// copy with regular name
156+
[
157+
// components
158+
"components/common/ReferenceLinks.tsx",
159+
160+
// error
161+
"error/SubmissionError.ts",
162+
163+
// interfaces
164+
"interfaces/Collection.ts",
165+
166+
// utils
167+
"utils/dataAccess.ts"
168+
].forEach(file => this.createFile(file, `${dir}/${file}`, context, false));
169+
170+
// API config
171+
this.createEntrypoint(api.entrypoint, `${dir}/config/entrypoint.ts`);
172+
173+
if (serverPath) {
174+
this.createExpressRoute(serverPath, lc, this.getShowRoute(lc));
175+
}
176+
}
177+
178+
getShowRoute(name) {
179+
return `server.get('/${name}/:id', (req, res) => {
180+
return app.render(req, res, '/${name}', { id: req.params.id })
181+
});`;
182+
}
183+
184+
createExpressRoute(path, resourceName, toInsert) {
185+
const content = fs.readFileSync(path, "utf-8");
186+
const code = parse(content);
187+
const { namedTypes } = types;
188+
189+
types.visit(code, {
190+
visitExpressionStatement: function(path) {
191+
const args = path.value.expression.arguments;
192+
if (
193+
2 === args.length &&
194+
namedTypes.Literal.check(args[0]) &&
195+
"*" === args[0].value &&
196+
namedTypes.ArrowFunctionExpression.check(args[1])
197+
) {
198+
// insert route before "*" route
199+
path.parent.value.body.splice(path.name, 0, toInsert);
200+
201+
return false;
202+
}
203+
204+
this.traverse(path);
205+
}
206+
});
207+
208+
fs.writeFileSync(path, print(code).code);
209+
console.log(
210+
chalk.green("'Show' route for %s has been added to your server"),
211+
resourceName
212+
);
213+
this.routeAddedtoServer = true;
214+
}
215+
216+
getDescription(field) {
217+
return field.description ? field.description.replace(/"/g, "'") : "";
218+
}
219+
220+
parseFields(resource) {
221+
const fields = [
222+
...resource.writableFields,
223+
...resource.readableFields
224+
].reduce((list, field) => {
225+
if (list[field.name]) {
226+
return list;
227+
}
228+
229+
return {
230+
...list,
231+
[field.name]: {
232+
notrequired: !field.required,
233+
name: field.name,
234+
type: this.getType(field),
235+
description: this.getDescription(field),
236+
readonly: false,
237+
reference: field.reference
238+
}
239+
};
240+
}, {});
241+
242+
// Parse fields to add relevant imports, required for Typescript
243+
const fieldsArray = Object.values(fields);
244+
const imports = Object.values(fields).reduce(
245+
(list, { reference, type }) => {
246+
if (!reference) {
247+
return list;
248+
}
249+
250+
return {
251+
...list,
252+
[type]: {
253+
type,
254+
file: `./${type}`
255+
}
256+
};
257+
},
258+
{}
259+
);
260+
261+
return { fields: fieldsArray, imports: Object.values(imports) };
262+
}
263+
264+
ucFirst(target) {
265+
return target.charAt(0).toUpperCase() + target.slice(1);
266+
}
267+
}

0 commit comments

Comments
 (0)