Skip to content
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

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