Skip to content

feat: add next.js generator #130

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 4 commits into from
Jun 11, 2019
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"handlebars-helpers": "^0.10.0",
"isomorphic-fetch": "^2.2.1",
"mkdirp": "^0.5.1",
"recast": "^0.18.1",
"sprintf-js": "^1.1.1"
},
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions src/generators.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AdminOnRestGenerator from "./generators/AdminOnRestGenerator";
import NextGenerator from "./generators/NextGenerator";
import ReactGenerator from "./generators/ReactGenerator";
import ReactNativeGenerator from "./generators/ReactNativeGenerator";
import TypescriptInterfaceGenerator from "./generators/TypescriptInterfaceGenerator";
Expand All @@ -14,6 +15,8 @@ export default function generators(generator = "react") {
switch (generator) {
case "admin-on-rest":
return wrap(AdminOnRestGenerator);
case "next":
return wrap(NextGenerator);
case "react":
return wrap(ReactGenerator);
case "react-native":
Expand Down
55 changes: 54 additions & 1 deletion src/generators/BaseGenerator.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import chalk from "chalk";
import fs from "fs";
import handlebars from "handlebars";
import mkdirp from "mkdirp";
Expand Down Expand Up @@ -30,7 +31,9 @@ export default class {
return;
}

if (warn) console.log(`The directory "${dir}" already exists`);
if (warn) {
console.log(chalk.yellow(`The directory "${dir}" already exists`));
}
}

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

// eslint-disable-next-line no-unused-vars
checkDependencies(dir) {}

getTargetDependencies(dir) {
const packageFilePath = `${dir}/package.json`;
let packageFile;
let dependencies = [];
try {
if (!fs.existsSync(packageFilePath)) {
throw new Error();
}
packageFile = fs.readFileSync(packageFilePath);
const configuration = JSON.parse(packageFile.toString());
dependencies = Object.keys({
...configuration.dependencies,
...configuration.devDependencies
});
} catch (e) {
console.log(
chalk.yellow(
"There's no readable package file in the target directory. Generator can't check dependencies."
)
);
}

return dependencies;
}

getHtmlInputTypeFromField(field) {
switch (field.id) {
case "http://schema.org/email":
Expand Down Expand Up @@ -88,6 +119,28 @@ export default class {
}
}

getType(field) {
if (field.reference) {
return field.reference.title;
}

switch (field.range) {
case "http://www.w3.org/2001/XMLSchema#integer":
case "http://www.w3.org/2001/XMLSchema#decimal":
return "number";
case "http://www.w3.org/2001/XMLSchema#boolean":
return "boolean";
case "http://www.w3.org/2001/XMLSchema#date":
case "http://www.w3.org/2001/XMLSchema#dateTime":
case "http://www.w3.org/2001/XMLSchema#time":
return "Date";
case "http://www.w3.org/2001/XMLSchema#string":
return "string";
}

return "any";
}

buildFields(fields) {
return fields.map(field => ({
...field,
Expand Down
267 changes: 267 additions & 0 deletions src/generators/NextGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import chalk from "chalk";
import fs from "fs";
import BaseGenerator from "./BaseGenerator";
import { parse, print, types } from "recast";

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

this.routeAddedtoServer = false;
this.registerTemplates(`next/`, [
// components
"components/common/ReferenceLinks.tsx",
"components/foo/List.tsx",
"components/foo/ListItem.tsx",
"components/foo/Show.tsx",

// interfaces
"error/SubmissionError.ts",

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

// pages
"pages/foo.tsx",
"pages/foos.tsx",

// utils
"utils/dataAccess.ts"
]);
}

checkDependencies(dir, serverPath) {
const dependencies = this.getTargetDependencies(dir);

if (!dependencies.length) {
return;
}

if (!dependencies.includes("@zeit/next-typescript")) {
console.log(
chalk.yellow(
"It seems next-typescript is not installed but generator needs typescript to work efficiently."
)
);
}

if (!dependencies.includes("express")) {
console.log(
chalk.yellow(
"It seems express is not installed but generator needs a custom express server to work efficiently."
)
);
}

if (serverPath) {
if (!fs.existsSync(serverPath)) {
console.log(chalk.red("Express server file doesn't exists."));
return;
}

const { mode } = fs.statSync(serverPath);
if ("200" !== (mode & parseInt("200", 8)).toString(8)) {
console.log(chalk.red("Express server file is not writable."));
}
}
}

checkImports(directory, imports, extension = ".ts") {
imports.forEach(({ file }) => {
if (fs.existsSync(directory + file + extension)) {
return;
}

console.log(
chalk.yellow(
'An import for the file "%s" has been generated but the file doesn\'t exists.'
),
file
);
});
}

help(resource, dir) {
console.log(
chalk.green('Code for the "%s" resource type has been generated!'),
resource.title
);

// missing import
const { imports } = this.parseFields(resource);
this.checkImports(`${dir}/interfaces/`, imports);

// server route configuration
if (!this.routeAddedtoServer) {
const lc = resource.title.toLowerCase();
console.log(
"Paste the following route to your server configuration file:"
);
console.log(chalk.green(this.getShowRoute(lc)));
}
}

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

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

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

// copy with patterned name
this.createDir(`${dir}/components/${context.lc}`);
[
// components
"components/%s/List.tsx",
"components/%s/ListItem.tsx",
"components/%s/Show.tsx",

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

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

// copy with regular name
[
// components
"components/common/ReferenceLinks.tsx",

// error
"error/SubmissionError.ts",

// interfaces
"interfaces/Collection.ts",

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

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

if (serverPath) {
this.createExpressRoute(serverPath, lc, this.getShowRoute(lc));
}
}

getShowRoute(name) {
return `server.get('/${name}/:id', (req, res) => {
return app.render(req, res, '/${name}', { id: req.params.id })
});`;
}

createExpressRoute(path, resourceName, toInsert) {
const content = fs.readFileSync(path, "utf-8");
const code = parse(content);
const { namedTypes } = types;

types.visit(code, {
visitExpressionStatement: function(path) {
const args = path.value.expression.arguments;
if (
2 === args.length &&
namedTypes.Literal.check(args[0]) &&
"*" === args[0].value &&
namedTypes.ArrowFunctionExpression.check(args[1])
) {
// insert route before "*" route
path.parent.value.body.splice(path.name, 0, toInsert);

return false;
}

this.traverse(path);
}
});

fs.writeFileSync(path, print(code).code);
console.log(
chalk.green("'Show' route for %s has been added to your server"),
resourceName
);
this.routeAddedtoServer = true;
}

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);
}
}
Loading