Skip to content
New issue

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

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

Already on GitHub? # to your account

feat: Angular generator #386

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
14,484 changes: 14,484 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"tmp": "^0.2.0"
},
"dependencies": {
"@angular/cli": "^18.0.6",
"@api-platform/api-doc-parser": "^0.16.0",
"@babel/runtime": "^7.0.0",
"chalk": "^5.0.0",
Expand Down Expand Up @@ -67,14 +68,13 @@
"test-react-app": "./testapp.sh react",
"test-next-app": "./testapp.sh next",
"test-vue-app": "./testapp.sh vue",
"test-nuxt-app": "./testapp.sh nuxt"
"test-nuxt-app": "./testapp.sh nuxt",
"test-angular-app": "./testapp.sh angular"
},
"lint-staged": {
"src/**/*.js": "yarn lint --fix"
},
"bin": {
"create-client": "./lib/index.js"
},
"bin": "./lib/index.js",
"publishConfig": {
"access": "public"
}
Expand Down
3 changes: 3 additions & 0 deletions src/generators.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import TypescriptInterfaceGenerator from "./generators/TypescriptInterfaceGenera
import VueGenerator from "./generators/VueGenerator.js";
import VuetifyGenerator from "./generators/VuetifyGenerator.js";
import QuasarGenerator from "./generators/QuasarGenerator.js";
import AngularGenerator from "./generators/AngularGenerator.js";

function wrap(cl) {
return ({ hydraPrefix, templateDirectory }) =>
Expand Down Expand Up @@ -36,5 +37,7 @@ export default async function generators(generator = "react") {
return wrap(VuetifyGenerator);
case "quasar":
return wrap(QuasarGenerator);
case "angular":
return wrap(AngularGenerator);
}
}
254 changes: 254 additions & 0 deletions src/generators/AngularGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import BaseGenerator from "./BaseGenerator.js";
import handlebars from "handlebars";
import hbhComparison from "handlebars-helpers/lib/comparison.js";
import hbhString from "handlebars-helpers/lib/string.js";
import chalk from "chalk";

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

this.registerTemplates("common/", [
// utils
"utils/config.ts",
"utils/date.ts",
]);

this.registerTemplates("angular/", [
// COMMON COMPONENTS
"app/components/common/delete/delete.component.html",
"app/components/common/delete/delete.component.ts",
"app/components/common/header/header.component.css",
"app/components/common/header/header.component.html",
"app/components/common/header/header.component.ts",
"app/components/common/layout/layout.component.html",
"app/components/common/layout/layout.component.ts",
"app/components/common/pagination/pagination.component.html",
"app/components/common/pagination/pagination.component.ts",
"app/components/common/sidebar/sidebar.component.css",
"app/components/common/sidebar/sidebar.component.html",
"app/components/common/sidebar/sidebar.component.ts",
"app/components/common/svg/list-svg/list-svg.component.svg",
"app/components/common/svg/list-svg/list-svg.component.ts",
"app/components/common/svg/show-svg/show-svg.component.svg",
"app/components/common/svg/show-svg/show-svg.component.ts",
"app/components/common/svg/edit-svg/edit-svg.component.svg",
"app/components/common/svg/edit-svg/edit-svg.component.ts",
"app/components/common/svg/menu/menu.component.svg",
"app/components/common/svg/menu/menu.component.ts",
"app/components/common/back-to-list/back-to-list.component.html",
"app/components/common/back-to-list/back-to-list.component.ts",
"app/components/common/alert/alert.component.html",
"app/components/common/alert/alert.component.ts",

// COMPONENTS
"app/components/foo/create/create.component.html",
"app/components/foo/create/create.component.ts",
"app/components/foo/edit/edit.component.html",
"app/components/foo/edit/edit.component.ts",
"app/components/foo/form/form.component.html",
"app/components/foo/form/form.component.ts",
"app/components/foo/list/list.component.html",
"app/components/foo/list/list.component.ts",
"app/components/foo/show/show.component.html",
"app/components/foo/show/show.component.ts",
"app/components/foo/table/table.component.html",
"app/components/foo/table/table.component.ts",
"app/app.component.html",
"app/app.component.ts",

// CONFIG
"app/app.config.ts",

//INTERFACE
"app/interface/api.ts",

// ROUTER
"app/router/foo.ts",
"app/router/index.ts",
"app/app.routes.ts",

//SERVICE
"app/service/api.service.ts",
]);

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

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.js` by default):"
);
console.log(
chalk.green(`
// import reducers
import ${titleLc} from './reducers/${titleLc}/';

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

generate(api, resource, dir) {
const lc = resource.title.toLowerCase();
const titleUcFirst =
resource.title.charAt(0).toUpperCase() + resource.title.slice(1);
const fields = this.parseFields(resource);
const hasIsRelation = fields.some((field) => field.isRelation);
const hasIsRelations = fields.some((field) => field.isRelations);
const hasDateField = fields.some((field) => field.type === "dateTime");
const formFields = this.buildFields(fields);

const context = {
title: resource.title,
name: resource.name,
lc,
uc: resource.title.toUpperCase(),
fields,
formFields,
hydraPrefix: this.hydraPrefix,
titleUcFirst,
hasIsRelation,
hasIsRelations,
hasRelations: hasIsRelation || hasIsRelations,
hasDateField,
apiResource: this.apiResource(api),
};

//CREATE DIRECTORIES - These directories may already exist
[
`${dir}/app/components/${lc}/create`,
`${dir}/app/components/${lc}/edit`,
`${dir}/app/components/${lc}/form`,
`${dir}/app/components/${lc}/list`,
`${dir}/app/components/${lc}/show`,
`${dir}/app/components/${lc}/table`,
`${dir}/app/components/common/alert`,
`${dir}/app/components/common/back-to-list`,
`${dir}/app/components/common/delete`,
`${dir}/app/components/common/header`,
`${dir}/app/components/common/layout`,
`${dir}/app/components/common/pagination`,
`${dir}/app/components/common/sidebar`,
`${dir}/app/components/common/svg`,
`${dir}/app/components/common/svg/list-svg`,
`${dir}/app/components/common/svg/show-svg`,
`${dir}/app/components/common/svg/edit-svg`,
`${dir}/app/components/common/svg/menu`,
`${dir}/app/interface`,
`${dir}/app/router`,
`${dir}/app/service`,
`${dir}/app/utils`,
].forEach((dir) => this.createDir(dir, false));

//CREATE FILE
[
"app/components/common/svg/list-svg/list-svg.component.svg",
"app/components/common/svg/list-svg/list-svg.component.ts",
"app/components/common/svg/show-svg/show-svg.component.svg",
"app/components/common/svg/show-svg/show-svg.component.ts",
"app/components/common/svg/edit-svg/edit-svg.component.svg",
"app/components/common/svg/edit-svg/edit-svg.component.ts",
"app/components/common/svg/menu/menu.component.svg",
"app/components/common/svg/menu/menu.component.ts",
"app/components/common/delete/delete.component.html",
"app/components/common/delete/delete.component.ts",
"app/components/common/header/header.component.css",
"app/components/common/header/header.component.html",
"app/components/common/header/header.component.ts",
"app/components/common/layout/layout.component.html",
"app/components/common/layout/layout.component.ts",
"app/components/common/pagination/pagination.component.html",
"app/components/common/pagination/pagination.component.ts",
"app/components/common/sidebar/sidebar.component.css",
"app/components/common/sidebar/sidebar.component.html",
"app/components/common/sidebar/sidebar.component.ts",
"app/components/common/back-to-list/back-to-list.component.html",
"app/components/common/back-to-list/back-to-list.component.ts",
"app/components/common/alert/alert.component.html",
"app/components/common/alert/alert.component.ts",
"app/interface/api.ts",
"app/service/api.service.ts",
"app/router/index.ts",
"app/app.component.html",
"app/app.component.ts",
"app/app.config.ts",
"app/app.routes.ts",
].forEach((file) =>
this.createFile(file, `${dir}/${file}`, context, false)
);

// DYNAMIC FILE
[
"app/router/%s.ts",
"app/components/%s/list/list.component.html",
"app/components/%s/list/list.component.ts",
"app/components/%s/create/create.component.html",
"app/components/%s/create/create.component.ts",
"app/components/%s/edit/edit.component.html",
"app/components/%s/edit/edit.component.ts",
"app/components/%s/form/form.component.html",
"app/components/%s/form/form.component.ts",
"app/components/%s/show/show.component.html",
"app/components/%s/show/show.component.ts",
"app/components/%s/show/show.component.html",
"app/components/%s/table/table.component.html",
"app/components/%s/table/table.component.ts",
].forEach((file) =>
this.createFileFromPattern(file, dir, [lc, formFields], context)
);

//UTILS
["utils/date.ts"].forEach((path) =>
this.createFile(path, `${dir}/app/${path}`, context, false)
);

// CONFIG
this.createConfigFile(`${dir}/app/utils/config.ts`, {
entrypoint: api.entrypoint,
});
}

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

const isReferences = Boolean(
field.reference && field.maxCardinality !== 1
);
const isEmbeddeds = Boolean(field.embedded && field.maxCardinality !== 1);

return {
...list,
[field.name]: {
...field,
isReferences,
isEmbeddeds,
isRelation: field.reference || field.embedded,
isRelations: isEmbeddeds || isReferences,
},
};
}, {});

return Object.values(fields);
}

apiResource(api) {
return api.resources
.filter((val) => !val.deprecated)
.map((val) => val.title);
}
}
97 changes: 97 additions & 0 deletions src/generators/AngularGenerator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Api, Resource, Field } from "@api-platform/api-doc-parser";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";
import tmp from "tmp";
import AngularGenerator from "./AngularGenerator.js";

const dirname = path.dirname(fileURLToPath(import.meta.url));

const generator = new AngularGenerator({
hydraPrefix: "hydra:",
templateDirectory: `${dirname}/../../templates`,
});

afterEach(() => {
jest.resetAllMocks();
});

describe("generate", () => {
test("Generate an Angular app", () => {
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);

[
"app/components/common/delete/delete.component.html",
"app/components/common/delete/delete.component.ts",
"app/components/common/header/header.component.css",
"app/components/common/header/header.component.html",
"app/components/common/header/header.component.ts",
"app/components/common/layout/layout.component.html",
"app/components/common/layout/layout.component.ts",
"app/components/common/pagination/pagination.component.html",
"app/components/common/pagination/pagination.component.ts",
"app/components/common/sidebar/sidebar.component.css",
"app/components/common/sidebar/sidebar.component.html",
"app/components/common/sidebar/sidebar.component.ts",
"app/components/common/svg/list-svg/list-svg.component.svg",
"app/components/common/svg/list-svg/list-svg.component.ts",
"app/components/common/svg/show-svg/show-svg.component.svg",
"app/components/common/svg/show-svg/show-svg.component.ts",
"app/components/common/svg/edit-svg/edit-svg.component.svg",
"app/components/common/svg/edit-svg/edit-svg.component.ts",
"app/components/common/svg/menu/menu.component.svg",
"app/components/common/svg/menu/menu.component.ts",
"app/components/common/back-to-list/back-to-list.component.html",
"app/components/common/back-to-list/back-to-list.component.ts",
"app/components/common/alert/alert.component.html",
"app/components/common/alert/alert.component.ts",
"app/interface/api.ts",
"app/router/foo.ts",
"app/service/api.service.ts",
].forEach((file) => expect(fs.existsSync(tmpobj.name + file)).toBe(true));

[
"app/router/abc.ts",
"app/components/abc/list/list.component.html",
"app/components/abc/list/list.component.ts",
"app/components/abc/create/create.component.html",
"app/components/abc/create/create.component.ts",
"app/components/abc/edit/edit.component.html",
"app/components/abc/edit/edit.component.ts",
"app/components/abc/form/form.component.html",
"app/components/abc/form/form.component.ts",
"app/components/abc/show/show.component.html",
"app/components/abc/show/show.component.ts",
"app/components/abc/show/show.component.html",
"app/components/abc/table/table.component.html",
"app/components/abc/table/table.component.ts",
].forEach((file) => {
expect(fs.existsSync(tmpobj.name + file)).toBe(true);
expect(fs.readFileSync(tmpobj.name + file, "utf8")).toMatch(/bar/);
});

tmpobj.removeCallback();
});
});
Loading
Loading