Skip to content

Commit

Permalink
feat: new version for react native generator
Browse files Browse the repository at this point in the history
  • Loading branch information
29Hido committed Mar 15, 2024
1 parent cb47c8b commit 6f5967b
Show file tree
Hide file tree
Showing 20 changed files with 870 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/generators.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import NextGenerator from "./generators/NextGenerator.js";
import NuxtGenerator from "./generators/NuxtGenerator.js";
import ReactGenerator from "./generators/ReactGenerator.js";
import ReactNativeGenerator from "./generators/ReactNativeGenerator.js";
import ReactNativeGeneratorV2 from "./generators/ReactNativeGeneratorV2.js";
import TypescriptInterfaceGenerator from "./generators/TypescriptInterfaceGenerator.js";
import VueGenerator from "./generators/VueGenerator.js";
import VuetifyGenerator from "./generators/VuetifyGenerator.js";
Expand All @@ -28,6 +29,8 @@ export default async function generators(generator = "react") {
return wrap(ReactGenerator);
case "react-native":
return wrap(ReactNativeGenerator);
case "react-native-v2":
return wrap(ReactNativeGeneratorV2);
case "typescript":
return wrap(TypescriptInterfaceGenerator);
case "vue":
Expand Down
188 changes: 188 additions & 0 deletions src/generators/ReactNativeGeneratorV2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import chalk from "chalk";
import handlebars from "handlebars";
import BaseGenerator from "./BaseGenerator.js";
import hbhComparison from "handlebars-helpers/lib/comparison.js";

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

handlebars.registerHelper("ifNotResource", function (item, options) {
if (item === null) {
return options.fn(this);
}
return options.inverse(this);
});

this.registerTemplates(`react-native-v2/`, [
"app/(tabs)/foos.tsx",
"app/_layout.tsx.dist",
"lib/hooks.ts",
"lib/store.ts",
"lib/types/ApiResource.ts",
"lib/types/HydraView.ts",
"lib/types/Logs.ts",
"lib/types/foo.ts",
"lib/factory/logFactory.ts",
"lib/slices/fooSlice.ts",
"lib/api/fooApi.ts",
"components/Main.tsx",
"components/Navigation.tsx",
"components/StoreProvider.tsx",
"components/foo/CreateEditModal.tsx",
"components/foo/Form.tsx",
"components/foo/LogsRenderer.tsx",
]);

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

help(resource) {
const titleLc = resource.title.toLowerCase();

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

console.log("You must now configure the lib/store.ts");
console.log(
chalk.green(`
// imports for ${titleLc}
import ${titleLc}Slice from './slices/${titleLc}Slice';
import { ${titleLc}Api } from './api/${titleLc}Api';
// reducer for ${titleLc}
reducer: {
...
${titleLc}: ${titleLc}Slice,
[${titleLc}Api.reducerPath]: ${titleLc}Api.reducer,
}
// middleware for ${titleLc}
getDefaultMiddleware().concat(..., ${titleLc}Api.middleware)
`)
);

console.log(
"You should replace app/_layout.tsx by the generated one and add the following route:"
);
console.log(
chalk.green(`
<Tabs.Screen
name="(tabs)/${titleLc}s"
options={options.tabs.${titleLc}}
/>
tabs: {
...
${titleLc}: {
title: '${titleLc}',
headerShown: false,
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
},
}
`)
);
}

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 context = {
title: resource.title,
name: resource.name,
lc,
uc: resource.title.toUpperCase(),
fields,
formFields: this.buildFields(fields),
hydraPrefix: this.hydraPrefix,
ucf: titleUcFirst,
};

// Create directories
// These directories may already exist
[
`${dir}/app/(tabs)`,
`${dir}/config`,
`${dir}/components`,
`${dir}/components/${lc}`,
`${dir}/lib`,
`${dir}/lib/api`,
`${dir}/lib/factory`,
`${dir}/lib/slices`,
`${dir}/lib/types`,
].forEach((dir) => this.createDir(dir, false));

// static files
[
"lib/hooks.ts",
"lib/store.ts",
"lib/types/ApiResource.ts",
"lib/types/HydraView.ts",
"lib/types/Logs.ts",
"lib/factory/logFactory.ts",
"components/Main.tsx",
"components/Navigation.tsx",
"components/StoreProvider.tsx",
].forEach((file) => this.createFile(file, `${dir}/${file}`));

// templated files ucFirst
["lib/types/%s.ts"].forEach((pattern) =>
this.createFileFromPattern(pattern, dir, [titleUcFirst], context)
);

// templated files lc
[
"app/(tabs)/%ss.tsx",
"app/_layout.tsx.dist",
"lib/slices/%sSlice.ts",
"lib/api/%sApi.ts",
"components/%s/CreateEditModal.tsx",
"components/%s/Form.tsx",
"components/%s/LogsRenderer.tsx",
].forEach((pattern) =>
this.createFileFromPattern(pattern, dir, [lc], context)
);

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

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

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

return {
...list,
[field.name]: {
...field,
type: this.getType(field),
description: this.getDescription(field),
readonly: false,
isReferences,
isEmbeddeds,
isRelations: isEmbeddeds || isReferences,
},
};
}, {});

return Object.values(fields);
}
}
60 changes: 60 additions & 0 deletions src/generators/ReactNativeGeneratorV2.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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 ReactNativeGeneratorV2 from "./ReactNativeGeneratorV2.js";

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

test("Generate a React Native V2 app", () => {
const generator = new ReactNativeGeneratorV2({
hydraPrefix: "hydra:",
templateDirectory: `${dirname}/../../templates`,
});
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);

[
"/lib/hooks.ts",
"/lib/store.ts",
"/lib/types/ApiResource.ts",
"/lib/types/HydraView.ts",
"/lib/types/Logs.ts",
"/lib/factory/logFactory.ts",
"/components/Main.tsx",
"/components/Navigation.tsx",
"/components/StoreProvider.tsx",
"/app/_layout.tsx.dist",

"/app/(tabs)/abcs.tsx",
"/lib/slices/abcSlice.ts",
"/lib/api/abcApi.ts",
"/components/abc/CreateEditModal.tsx",
"/components/abc/Form.tsx",
"/components/abc/LogsRenderer.tsx",
].forEach((file) => expect(fs.existsSync(tmpobj.name + file)).toBe(true));

tmpobj.removeCallback();
});
73 changes: 73 additions & 0 deletions templates/react-native-v2/app/(tabs)/foos.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Main from "@/components/Main";
import Navigation from "@/components/Navigation";
import CreateEditModal from "@/components/{{{lc}}}/CreateEditModal";
import LogsRenderer from "@/components/{{{lc}}}/LogsRenderer";
import { useLazyGetAllQuery } from "@/lib/api/{{{lc}}}Api";
import { useAppDispatch, useAppSelector } from "@/lib/hooks";
import { setCurrentData, setData, setModalIsEdit, setModalIsVisible, setPage, setView } from "@/lib/slices/{{{lc}}}Slice";
import {{{ucf}}} from "@/lib/types/{{{ucf}}}";
import { useLocalSearchParams } from "expo-router";
import { useEffect } from "react";
import { Pressable, ScrollView, Text, View } from "react-native";

export default function {{{ucf}}}s() {
const datas = useAppSelector(state => state.{{lc}}.data);
const view = useAppSelector(state => state.{{{lc}}}.view);
const { page = '1' } = useLocalSearchParams<{ page: string }>();

const dispatch = useAppDispatch();
const [getAll] = useLazyGetAllQuery();

const toggleEditModal = (data: {{{ucf}}}) => {
dispatch(setCurrentData(data));
dispatch(setModalIsVisible(true));
dispatch(setModalIsEdit(true));
};

const toggleCreateModal = () => {
dispatch(setModalIsVisible(true));
dispatch(setModalIsEdit(false));
}

useEffect(() => {
const intPage = parseInt(page);
if (intPage < 0) return;
dispatch(setPage(intPage));
getAll(intPage)
.unwrap()
.then(fulfilled => {
dispatch(setView(fulfilled["hydra:view"]));
dispatch(setData(fulfilled["hydra:member"]));
})
}, [page]);

return (
<Main>
<View className="py-3 flex flex-row items-center justify-between">
<Text className="text-3xl">{{{ucf}}}s List</Text>
<Pressable onPress={() => toggleCreateModal()}>
<Text className="bg-cyan-500 cursor-pointer text-white text-sm font-bold py-2 px-4 rounded">Create</Text>
</Pressable>
</View>
<ScrollView>
<LogsRenderer />
<View>
{
datas.map(data => (
<Pressable onPress={() => toggleEditModal(data)} key={data["@id"]}>
<View className="flex flex-column my-2 block max-w p-6 bg-white border border-gray-300 rounded shadow">
<Text>ID: {data['@id']}</Text>
{{#each fields}}
<Text>{{{name}}}: {data["{{{name}}}"]}</Text>
{{/each}}
</View>
</Pressable>
))
}
</View>
<CreateEditModal />
</ScrollView>
<Navigation view={view} />
</Main >
);
}
39 changes: 39 additions & 0 deletions templates/react-native-v2/app/_layout.tsx.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import { FontAwesome } from "@expo/vector-icons";
import "../global.css";
import { Tabs } from "expo-router";
import StoreProvider from '@/components/StoreProvider';

function TabBarIcon(props: {
name: React.ComponentProps<typeof FontAwesome>['name'];
color: string;
}) {
return <FontAwesome size={28} style={iconMargin} {...props} />;
}
const iconMargin = { marginBottom: -3 }

export default function Layout() {
return (
<StoreProvider>
<Tabs screenOptions={options.tabsContainer}>
<Tabs.Screen
name="index"
options={options.tabs.home}
/>
</Tabs>
</StoreProvider>
)
}

const options = {
tabsContainer: {
headerShown: false,
tabBarShowLabel: false,
},
tabs: {
home: {
title: 'Accueil',
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
},
}
};
16 changes: 16 additions & 0 deletions templates/react-native-v2/components/Main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { View } from "react-native";

export default function Main({ children }) {
return (
<View className="flex flex-1 py-16" style={styles.container}>
{children}
</View>
)
}

const styles = {
container: {
position: 'relative',
marginHorizontal: '3%',
}
}
Loading

0 comments on commit 6f5967b

Please sign in to comment.