@nano-app/frontend 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/ApiClient.d.ts +16 -0
- package/dist/api/ApiClient.js +69 -0
- package/dist/api/QueryBuilder.d.ts +8 -0
- package/dist/api/QueryBuilder.js +51 -0
- package/dist/components/Controller.d.ts +16 -0
- package/dist/components/Controller.js +29 -0
- package/dist/config/index.d.ts +21 -0
- package/dist/config/index.js +10 -0
- package/dist/hooks/useComponent.d.ts +8 -0
- package/dist/hooks/useComponent.js +10 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +29 -0
- package/dist/plugin/index.d.ts +11 -0
- package/dist/plugin/index.js +134 -0
- package/dist/store/StoreClient.d.ts +25 -0
- package/dist/store/StoreClient.js +65 -0
- package/dist/store/StoreManager.d.ts +11 -0
- package/dist/store/StoreManager.js +61 -0
- package/dist/store/bootstrapApp.d.ts +19 -0
- package/dist/store/bootstrapApp.js +25 -0
- package/dist/store/setStores.d.ts +8 -0
- package/dist/store/setStores.js +23 -0
- package/dist/types/api.d.ts +7 -0
- package/dist/types/api.js +2 -0
- package/dist/ui/Component.d.ts +4 -0
- package/dist/ui/Component.js +49 -0
- package/dist/ui/Controller.d.ts +14 -0
- package/dist/ui/Controller.js +12 -0
- package/dist/ui/StoreProvider.d.ts +18 -0
- package/dist/ui/StoreProvider.js +62 -0
- package/dist/ui/createComponent.d.ts +11 -0
- package/dist/ui/createComponent.js +40 -0
- package/dist/ui/defineController.d.ts +23 -0
- package/dist/ui/defineController.js +14 -0
- package/dist/ui/defineView.d.ts +3 -0
- package/dist/ui/defineView.js +44 -0
- package/dist/ui/definers.d.ts +1 -0
- package/dist/ui/definers.js +59 -0
- package/dist/ui/useComponent.d.ts +2 -0
- package/dist/ui/useComponent.js +9 -0
- package/dist/utils/Toast.d.ts +12 -0
- package/dist/utils/Toast.js +28 -0
- package/dist/utils/TokenHelper.d.ts +5 -0
- package/dist/utils/TokenHelper.js +17 -0
- package/package.json +48 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ApiResponse } from "@nano-app/backend";
|
|
2
|
+
import { StoreClient } from "../store/StoreClient";
|
|
3
|
+
export type ApiRequest = (query: string, body?: unknown) => Promise<ApiResponse>;
|
|
4
|
+
/**
|
|
5
|
+
* Cliente de API que extiende StoreClient y expone CRUD.
|
|
6
|
+
* @param baseUrl - Prefijo de todas las peticiones. Por defecto "/api".
|
|
7
|
+
*/
|
|
8
|
+
export declare class ApiClient extends StoreClient {
|
|
9
|
+
private baseUrl;
|
|
10
|
+
constructor(baseUrl?: string);
|
|
11
|
+
create: ApiRequest;
|
|
12
|
+
read: ApiRequest;
|
|
13
|
+
update: ApiRequest;
|
|
14
|
+
delete: ApiRequest;
|
|
15
|
+
private request;
|
|
16
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiClient = void 0;
|
|
4
|
+
const StoreClient_1 = require("../store/StoreClient");
|
|
5
|
+
const QueryBuilder_1 = require("./QueryBuilder");
|
|
6
|
+
const TokenHelper_1 = require("../utils/TokenHelper");
|
|
7
|
+
const Toast_1 = require("../utils/Toast");
|
|
8
|
+
/**
|
|
9
|
+
* Cliente de API que extiende StoreClient y expone CRUD.
|
|
10
|
+
* @param baseUrl - Prefijo de todas las peticiones. Por defecto "/api".
|
|
11
|
+
*/
|
|
12
|
+
class ApiClient extends StoreClient_1.StoreClient {
|
|
13
|
+
baseUrl;
|
|
14
|
+
constructor(baseUrl = "/api") {
|
|
15
|
+
super();
|
|
16
|
+
this.baseUrl = baseUrl;
|
|
17
|
+
}
|
|
18
|
+
create = async (entity, body) => {
|
|
19
|
+
return this.request("POST", entity, body);
|
|
20
|
+
};
|
|
21
|
+
read = async (query, params = {}) => {
|
|
22
|
+
const path = query.split("?")[0];
|
|
23
|
+
const response = await this.request("GET", QueryBuilder_1.QueryBuilder.build(query, params));
|
|
24
|
+
this.set(`/${path}`, response.result);
|
|
25
|
+
return response;
|
|
26
|
+
};
|
|
27
|
+
update = async (entity, body) => {
|
|
28
|
+
return this.request("PUT", entity, body);
|
|
29
|
+
};
|
|
30
|
+
delete = async (entity, id) => {
|
|
31
|
+
return this.request("DELETE", entity, id);
|
|
32
|
+
};
|
|
33
|
+
async request(method, path, body) {
|
|
34
|
+
let oResponse = {
|
|
35
|
+
result: null,
|
|
36
|
+
success: false,
|
|
37
|
+
message: "",
|
|
38
|
+
warning: false,
|
|
39
|
+
};
|
|
40
|
+
this.set("/busyCount", (this.get("/busyCount") || 0) + 1);
|
|
41
|
+
this.set("/busy", true);
|
|
42
|
+
try {
|
|
43
|
+
oResponse = await fetch(`${this.baseUrl}${path}`, {
|
|
44
|
+
method,
|
|
45
|
+
headers: {
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
Authorization: `Bearer ${TokenHelper_1.TokenHelper.get()}`,
|
|
48
|
+
},
|
|
49
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
50
|
+
})
|
|
51
|
+
.then((response) => response.json())
|
|
52
|
+
.then((response) => Toast_1.Toast.apiResponse(response));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error(error);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
this.set("/busyCount", this.get("/busyCount") - 1);
|
|
59
|
+
if (this.get("/busyCount") <= 0) {
|
|
60
|
+
this.set("/busy", false);
|
|
61
|
+
}
|
|
62
|
+
if (method !== "GET") {
|
|
63
|
+
this.set("/refresh", !this.get("/refresh"));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return oResponse;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.ApiClient = ApiClient;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QueryBuilder = void 0;
|
|
4
|
+
const DEFAULT_STEP_SKIP_TOP = 10;
|
|
5
|
+
class QueryBuilder {
|
|
6
|
+
static ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?Z?)?$/;
|
|
7
|
+
static buildFilters(filters = {}) {
|
|
8
|
+
const filterParts = [];
|
|
9
|
+
Object.entries(filters).forEach(([key, value]) => {
|
|
10
|
+
if (value !== undefined && value !== null) {
|
|
11
|
+
if (typeof value === "string") {
|
|
12
|
+
if (QueryBuilder.ISO_DATE_REGEX.test(value)) {
|
|
13
|
+
filterParts.push(`${key} ge ${value}`);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
filterParts.push(`${key} contains ${value}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
filterParts.push(`${key} eq ${value}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return filterParts.length > 0 ? filterParts.join(" and ") : "";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Construye una query OData a partir de un objeto de filtros.
|
|
28
|
+
*/
|
|
29
|
+
static build(path, params = {}) {
|
|
30
|
+
const { skip, filters, expand, orderby, top = 10, ...otherParams } = params;
|
|
31
|
+
const queryParams = Object.entries(otherParams).map(([key, value]) => `${key}=${value}`);
|
|
32
|
+
const filtersQuery = this.buildFilters(filters ?? {});
|
|
33
|
+
if (filtersQuery) {
|
|
34
|
+
queryParams.push(`filter=${filtersQuery}`);
|
|
35
|
+
}
|
|
36
|
+
if (expand) {
|
|
37
|
+
queryParams.push(`expand=${expand}`);
|
|
38
|
+
}
|
|
39
|
+
if (orderby) {
|
|
40
|
+
queryParams.push(`orderby=${orderby}`);
|
|
41
|
+
}
|
|
42
|
+
if (top) {
|
|
43
|
+
queryParams.push(`top=${top}`);
|
|
44
|
+
}
|
|
45
|
+
if (skip) {
|
|
46
|
+
queryParams.push(`skip=${skip}`);
|
|
47
|
+
}
|
|
48
|
+
return `${path}?${queryParams.join("&")}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.QueryBuilder = QueryBuilder;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { usePathname, useRouter } from "next/navigation";
|
|
2
|
+
import { ApiClient } from "../api/ApiClient";
|
|
3
|
+
import { StoreClient } from "../store/StoreClient";
|
|
4
|
+
export declare abstract class Controller {
|
|
5
|
+
private storeManager;
|
|
6
|
+
private isInitialized;
|
|
7
|
+
protected router?: ReturnType<typeof useRouter>;
|
|
8
|
+
protected pathname?: ReturnType<typeof usePathname>;
|
|
9
|
+
protected onInit(): void;
|
|
10
|
+
protected getStore(model: 'api'): ApiClient;
|
|
11
|
+
protected getStore(model?: 'app'): StoreClient;
|
|
12
|
+
protected getStore(model: string): ApiClient | StoreClient;
|
|
13
|
+
params: any;
|
|
14
|
+
deps(): any[];
|
|
15
|
+
render(params: any): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Controller = void 0;
|
|
4
|
+
const navigation_1 = require("next/navigation");
|
|
5
|
+
const StoreManager_1 = require("../store/StoreManager");
|
|
6
|
+
class Controller {
|
|
7
|
+
storeManager = new StoreManager_1.StoreManager();
|
|
8
|
+
isInitialized = false;
|
|
9
|
+
router;
|
|
10
|
+
pathname;
|
|
11
|
+
onInit() { }
|
|
12
|
+
getStore(model = 'app') {
|
|
13
|
+
return this.storeManager.getStore(model);
|
|
14
|
+
}
|
|
15
|
+
params = {};
|
|
16
|
+
deps() { return []; }
|
|
17
|
+
render(params) {
|
|
18
|
+
this.params = params;
|
|
19
|
+
this.router = (0, navigation_1.useRouter)();
|
|
20
|
+
this.pathname = (0, navigation_1.usePathname)();
|
|
21
|
+
// Inicializa el controlador si no ha sido inicializado
|
|
22
|
+
if (!this.isInitialized) {
|
|
23
|
+
this.isInitialized = true;
|
|
24
|
+
this.onInit();
|
|
25
|
+
this.storeManager.init();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.Controller = Controller;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type LocalStoreEntry = {
|
|
2
|
+
type: "local";
|
|
3
|
+
/** Path relativo a la raíz del proyecto. Ej: "src/models/app.json" */
|
|
4
|
+
path?: string;
|
|
5
|
+
/** Datos inline (alternativa a path) */
|
|
6
|
+
data?: Record<string, any>;
|
|
7
|
+
};
|
|
8
|
+
export type ApiStoreEntry = {
|
|
9
|
+
type: "api";
|
|
10
|
+
/** Prefijo base para todas las peticiones de este store. Ej: "/api/users" */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
};
|
|
13
|
+
export type StoreEntry = LocalStoreEntry | ApiStoreEntry;
|
|
14
|
+
export type NanoAppConfig = {
|
|
15
|
+
stores: Record<string, StoreEntry>;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Define la configuración del paquete con tipado completo.
|
|
19
|
+
* Usar en nano-app.config.ts en la raíz del proyecto.
|
|
20
|
+
*/
|
|
21
|
+
export declare function defineConfig(config: NanoAppConfig): NanoAppConfig;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defineConfig = defineConfig;
|
|
4
|
+
/**
|
|
5
|
+
* Define la configuración del paquete con tipado completo.
|
|
6
|
+
* Usar en nano-app.config.ts en la raíz del proyecto.
|
|
7
|
+
*/
|
|
8
|
+
function defineConfig(config) {
|
|
9
|
+
return config;
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Controller } from "../components/Controller";
|
|
2
|
+
type useComponentParams<CI> = {
|
|
3
|
+
controller: new () => CI;
|
|
4
|
+
view: (controller: CI) => React.ReactNode;
|
|
5
|
+
};
|
|
6
|
+
export type useComponent = <CI extends Controller>(p: useComponentParams<CI>) => (params?: Object) => React.ReactNode;
|
|
7
|
+
export declare const useComponent: useComponent;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useComponent = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const useComponent = ({ controller, view }) => (params = {}) => {
|
|
6
|
+
const c = (0, react_1.useMemo)(() => new controller(), [controller]);
|
|
7
|
+
c.render(params);
|
|
8
|
+
return (0, react_1.useMemo)(() => view(c), c.deps());
|
|
9
|
+
};
|
|
10
|
+
exports.useComponent = useComponent;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./api/ApiClient";
|
|
2
|
+
export * from "./api/QueryBuilder";
|
|
3
|
+
export { default as StoreManager } from "./store/StoreManager";
|
|
4
|
+
export * from "./store/StoreClient";
|
|
5
|
+
export * from "./ui/createComponent";
|
|
6
|
+
export * from "./ui/Controller";
|
|
7
|
+
export * from "./utils/Toast";
|
|
8
|
+
export * from "./utils/TokenHelper";
|
|
9
|
+
export type { ApiResponse } from "@nano-app/backend";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.StoreManager = void 0;
|
|
21
|
+
__exportStar(require("./api/ApiClient"), exports);
|
|
22
|
+
__exportStar(require("./api/QueryBuilder"), exports);
|
|
23
|
+
var StoreManager_1 = require("./store/StoreManager");
|
|
24
|
+
Object.defineProperty(exports, "StoreManager", { enumerable: true, get: function () { return __importDefault(StoreManager_1).default; } });
|
|
25
|
+
__exportStar(require("./store/StoreClient"), exports);
|
|
26
|
+
__exportStar(require("./ui/createComponent"), exports);
|
|
27
|
+
__exportStar(require("./ui/Controller"), exports);
|
|
28
|
+
__exportStar(require("./utils/Toast"), exports);
|
|
29
|
+
__exportStar(require("./utils/TokenHelper"), exports);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type NextConfig = Record<string, any>;
|
|
2
|
+
/**
|
|
3
|
+
* Envuelve la configuración de Next.js agregando el plugin de nano-app.
|
|
4
|
+
* Usar en next.config.ts:
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { withNanoApp } from "@nano-app/frontend/plugin";
|
|
8
|
+
* export default withNanoApp();
|
|
9
|
+
*/
|
|
10
|
+
export declare function withNanoApp(nextConfig?: NextConfig): NextConfig;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.withNanoApp = withNanoApp;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const VIRTUAL_MODULE_ID = "@nano-app/stores";
|
|
10
|
+
const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
|
|
11
|
+
/**
|
|
12
|
+
* Lee nano-app.config.ts desde la raíz del proyecto usando jiti
|
|
13
|
+
* para poder ejecutar TypeScript en Node.js sin compilar.
|
|
14
|
+
*/
|
|
15
|
+
function loadConfig(projectRoot) {
|
|
16
|
+
const configPath = path_1.default.join(projectRoot, "nano-app.config.ts");
|
|
17
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
18
|
+
throw new Error(`[nano-app] No se encontró nano-app.config.ts en ${projectRoot}`);
|
|
19
|
+
}
|
|
20
|
+
// jiti permite ejecutar TS directamente en Node.js (lo usa Next.js internamente)
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
|
+
const jiti = require("jiti")(projectRoot, { interopDefault: true });
|
|
23
|
+
return jiti(configPath);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resuelve los stores del config: lee los JSONs de los paths indicados
|
|
27
|
+
* y devuelve un mapa con todos los datos ya inlineados.
|
|
28
|
+
*/
|
|
29
|
+
function resolveStores(config, projectRoot) {
|
|
30
|
+
const resolved = {};
|
|
31
|
+
Object.entries(config.stores).forEach(([name, entry]) => {
|
|
32
|
+
if (entry.type === "local") {
|
|
33
|
+
if (entry.path) {
|
|
34
|
+
const filePath = path_1.default.join(projectRoot, entry.path);
|
|
35
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
36
|
+
throw new Error(`[nano-app] Store "${name}": no se encontró el archivo ${filePath}`);
|
|
37
|
+
}
|
|
38
|
+
const raw = fs_1.default.readFileSync(filePath, "utf-8");
|
|
39
|
+
resolved[name] = { type: "local", data: JSON.parse(raw) };
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
resolved[name] = { type: "local", data: entry.data ?? {} };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (entry.type === "api") {
|
|
46
|
+
resolved[name] = { type: "api", baseUrl: entry.baseUrl ?? "/api" };
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Genera el código del módulo virtual "@nano-app/stores"
|
|
53
|
+
* con todos los stores ya resueltos y serializados.
|
|
54
|
+
*/
|
|
55
|
+
function generateVirtualModule(stores) {
|
|
56
|
+
return `export const stores = ${JSON.stringify(stores, null, 2)};`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Plugin de Webpack que:
|
|
60
|
+
* 1. Lee nano-app.config.ts en build time
|
|
61
|
+
* 2. Resuelve los paths a JSONs
|
|
62
|
+
* 3. Inyecta un módulo virtual "@nano-app/stores" con todos los datos
|
|
63
|
+
*/
|
|
64
|
+
class NanoAppPlugin {
|
|
65
|
+
projectRoot;
|
|
66
|
+
constructor(projectRoot) {
|
|
67
|
+
this.projectRoot = projectRoot;
|
|
68
|
+
}
|
|
69
|
+
apply(compiler) {
|
|
70
|
+
const pluginName = "NanoAppPlugin";
|
|
71
|
+
const projectRoot = this.projectRoot;
|
|
72
|
+
compiler.hooks.normalModuleFactory.tap(pluginName, (factory) => {
|
|
73
|
+
factory.hooks.resolve.tap(pluginName, (resolveData) => {
|
|
74
|
+
if (resolveData.request === VIRTUAL_MODULE_ID) {
|
|
75
|
+
resolveData.request = RESOLVED_VIRTUAL_MODULE_ID;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
compiler.hooks.compilation.tap(pluginName, (compilation) => {
|
|
80
|
+
compilation.hooks.normalModuleLoader.tap(pluginName, (_, module) => {
|
|
81
|
+
if (module.resource === RESOLVED_VIRTUAL_MODULE_ID) {
|
|
82
|
+
module.loaders = [];
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
// Resuelve el módulo virtual a código generado
|
|
87
|
+
compiler.resolverFactory.hooks.resolver
|
|
88
|
+
.for("normal")
|
|
89
|
+
.tap(pluginName, (resolver) => {
|
|
90
|
+
resolver.hooks.resolve.tapAsync(pluginName, (request, resolveContext, callback) => {
|
|
91
|
+
if (request.request === RESOLVED_VIRTUAL_MODULE_ID) {
|
|
92
|
+
const config = loadConfig(projectRoot);
|
|
93
|
+
const stores = resolveStores(config, projectRoot);
|
|
94
|
+
const code = generateVirtualModule(stores);
|
|
95
|
+
return callback(null, {
|
|
96
|
+
path: RESOLVED_VIRTUAL_MODULE_ID,
|
|
97
|
+
query: "",
|
|
98
|
+
file: true,
|
|
99
|
+
module: true,
|
|
100
|
+
source: code,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
callback();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Envuelve la configuración de Next.js agregando el plugin de nano-app.
|
|
110
|
+
* Usar en next.config.ts:
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* import { withNanoApp } from "@nano-app/frontend/plugin";
|
|
114
|
+
* export default withNanoApp();
|
|
115
|
+
*/
|
|
116
|
+
function withNanoApp(nextConfig = {}) {
|
|
117
|
+
return {
|
|
118
|
+
...nextConfig,
|
|
119
|
+
webpack(config, options) {
|
|
120
|
+
const projectRoot = options.dir ?? process.cwd();
|
|
121
|
+
// Módulo virtual para los stores resueltos
|
|
122
|
+
config.plugins.push(new NanoAppPlugin(projectRoot));
|
|
123
|
+
// Resuelve symlinks relativos a la app consumidora (necesario para file: links)
|
|
124
|
+
config.resolve.symlinks = false;
|
|
125
|
+
// Permite que el módulo virtual sea importado
|
|
126
|
+
config.resolve.alias = {
|
|
127
|
+
...config.resolve.alias,
|
|
128
|
+
[VIRTUAL_MODULE_ID]: RESOLVED_VIRTUAL_MODULE_ID,
|
|
129
|
+
};
|
|
130
|
+
// Encadena con el webpack config del usuario si existe
|
|
131
|
+
return nextConfig.webpack?.(config, options) ?? config;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type StoreData = Record<string, any>;
|
|
2
|
+
/**
|
|
3
|
+
* Clase base para manejar el estado de Zustand
|
|
4
|
+
* Zustand solo almacena valores, los métodos se ejecutan desde la clase
|
|
5
|
+
*/
|
|
6
|
+
export declare class StoreClient {
|
|
7
|
+
private getStore;
|
|
8
|
+
private setFn;
|
|
9
|
+
private getFn;
|
|
10
|
+
constructor(initialData?: StoreData);
|
|
11
|
+
/**
|
|
12
|
+
* Ejecuta el hook para suscribirse al store (reactividad)
|
|
13
|
+
*/
|
|
14
|
+
use: () => StoreData;
|
|
15
|
+
/**
|
|
16
|
+
* Setter y Getter del store
|
|
17
|
+
*/
|
|
18
|
+
set(path: string, value: any): void;
|
|
19
|
+
get(path: string): any;
|
|
20
|
+
/**
|
|
21
|
+
* Maneja el estado (Set o Get) navegando por el path
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
private manageState;
|
|
25
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StoreClient = void 0;
|
|
4
|
+
const zustand_1 = require("zustand");
|
|
5
|
+
/**
|
|
6
|
+
* Clase base para manejar el estado de Zustand
|
|
7
|
+
* Zustand solo almacena valores, los métodos se ejecutan desde la clase
|
|
8
|
+
*/
|
|
9
|
+
class StoreClient {
|
|
10
|
+
getStore;
|
|
11
|
+
setFn = () => { };
|
|
12
|
+
getFn = () => ({});
|
|
13
|
+
constructor(initialData = {}) {
|
|
14
|
+
this.getStore = (0, zustand_1.create)()((set, get) => {
|
|
15
|
+
this.setFn = set;
|
|
16
|
+
this.getFn = get;
|
|
17
|
+
return initialData;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Ejecuta el hook para suscribirse al store (reactividad)
|
|
22
|
+
*/
|
|
23
|
+
use = () => this.getStore();
|
|
24
|
+
/**
|
|
25
|
+
* Setter y Getter del store
|
|
26
|
+
*/
|
|
27
|
+
set(path, value) {
|
|
28
|
+
if (!this.setFn)
|
|
29
|
+
return;
|
|
30
|
+
const { oNewState } = this.manageState(path, true, value);
|
|
31
|
+
this.setFn((state) => ({ ...state, ...oNewState }));
|
|
32
|
+
}
|
|
33
|
+
get(path) {
|
|
34
|
+
if (!this.getFn)
|
|
35
|
+
return undefined;
|
|
36
|
+
const { value } = this.manageState(path);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Maneja el estado (Set o Get) navegando por el path
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
manageState(sPath, isSet = false, value) {
|
|
44
|
+
// Obtiene el path y el último valor
|
|
45
|
+
const aPath = sPath.split("/").filter(s => s !== "");
|
|
46
|
+
const lastProp = aPath.pop() || "";
|
|
47
|
+
// Valida si es el path inicial (Utiliza todo el objeto)
|
|
48
|
+
const oNewState = aPath.length === 0
|
|
49
|
+
? this.getFn()
|
|
50
|
+
: // Llega al final del path generando objetos si es necesario
|
|
51
|
+
aPath.reduce((oObject, sKey) => {
|
|
52
|
+
if (!oObject[sKey]) {
|
|
53
|
+
oObject[sKey] = {};
|
|
54
|
+
}
|
|
55
|
+
return oObject[sKey];
|
|
56
|
+
}, this.getFn());
|
|
57
|
+
// Setea el nuevo valor si debe
|
|
58
|
+
if (isSet) {
|
|
59
|
+
oNewState[lastProp] = value;
|
|
60
|
+
}
|
|
61
|
+
// Retorna el nuevo estado y el valor de la property enviada
|
|
62
|
+
return { oNewState, value: lastProp ? oNewState[lastProp] : oNewState };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.StoreClient = StoreClient;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ApiClient } from "../api/ApiClient";
|
|
2
|
+
import { StoreClient } from "./StoreClient";
|
|
3
|
+
export type StoresMap = Record<string, ApiClient | StoreClient>;
|
|
4
|
+
export declare class StoreManager {
|
|
5
|
+
private stores;
|
|
6
|
+
constructor();
|
|
7
|
+
addStore(name: string, store: ApiClient | StoreClient): void;
|
|
8
|
+
getStore(name: string): ApiClient | StoreClient;
|
|
9
|
+
}
|
|
10
|
+
declare const _default: StoreManager;
|
|
11
|
+
export default _default;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.StoreManager = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const ApiClient_1 = require("../api/ApiClient");
|
|
9
|
+
const StoreClient_1 = require("./StoreClient");
|
|
10
|
+
let userConfig = {};
|
|
11
|
+
try {
|
|
12
|
+
// Carga nano-app.config.cjs desde la raíz del proyecto del usuario.
|
|
13
|
+
// .cjs es CommonJS puro — Node.js lo ejecuta sincrónicamente sin compilador.
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
15
|
+
const mod = require(path_1.default.join(process.cwd(), "nano-app.config.cjs"));
|
|
16
|
+
userConfig = mod?.default ?? mod;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// sin config → el manager empieza vacío
|
|
20
|
+
}
|
|
21
|
+
function readJsonPath(filePath) {
|
|
22
|
+
try {
|
|
23
|
+
// fs solo existe en Node.js — en el browser este bloque nunca se ejecuta
|
|
24
|
+
// porque el StoreManager ya fue inicializado en el servidor
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
26
|
+
const fs = require("fs");
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
|
+
const path = require("path");
|
|
29
|
+
const abs = path.resolve(process.cwd(), filePath);
|
|
30
|
+
return JSON.parse(fs.readFileSync(abs, "utf-8"));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
class StoreManager {
|
|
37
|
+
stores = {};
|
|
38
|
+
constructor() {
|
|
39
|
+
if (!userConfig?.stores)
|
|
40
|
+
return;
|
|
41
|
+
Object.entries(userConfig.stores).forEach(([name, entry]) => {
|
|
42
|
+
if (entry.type === "api") {
|
|
43
|
+
this.stores[name] = new ApiClient_1.ApiClient(entry.baseUrl ?? "/api");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const data = entry.path
|
|
47
|
+
? readJsonPath(entry.path)
|
|
48
|
+
: (entry.data ?? {});
|
|
49
|
+
this.stores[name] = new StoreClient_1.StoreClient(data);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
addStore(name, store) {
|
|
54
|
+
this.stores[name] = store;
|
|
55
|
+
}
|
|
56
|
+
getStore(name) {
|
|
57
|
+
return this.stores[name];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.StoreManager = StoreManager;
|
|
61
|
+
exports.default = new StoreManager();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { StoreData } from "./StoreClient";
|
|
2
|
+
export type StoreManifestEntry = {
|
|
3
|
+
type: "local" | string;
|
|
4
|
+
data: StoreData;
|
|
5
|
+
} | {
|
|
6
|
+
type: "api" | string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
data?: never;
|
|
9
|
+
};
|
|
10
|
+
export type AppManifest = {
|
|
11
|
+
stores: Record<string, StoreManifestEntry>;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Lee el manifest y registra todos los stores en el StoreManager.
|
|
15
|
+
* Debe llamarse una sola vez en el layout raíz de la app.
|
|
16
|
+
*/
|
|
17
|
+
export declare function bootstrapApp(manifest: {
|
|
18
|
+
stores: Record<string, any>;
|
|
19
|
+
}): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.bootstrapApp = bootstrapApp;
|
|
7
|
+
const ApiClient_1 = require("../api/ApiClient");
|
|
8
|
+
const StoreClient_1 = require("./StoreClient");
|
|
9
|
+
const StoreManager_1 = __importDefault(require("./StoreManager"));
|
|
10
|
+
/**
|
|
11
|
+
* Lee el manifest y registra todos los stores en el StoreManager.
|
|
12
|
+
* Debe llamarse una sola vez en el layout raíz de la app.
|
|
13
|
+
*/
|
|
14
|
+
function bootstrapApp(manifest) {
|
|
15
|
+
const { stores } = manifest;
|
|
16
|
+
Object.entries(stores).forEach(([name, config]) => {
|
|
17
|
+
if (config.type === "api") {
|
|
18
|
+
StoreManager_1.default.addStore(name, new ApiClient_1.ApiClient());
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
// type: "local" o cualquier otro — usa los datos provistos
|
|
22
|
+
StoreManager_1.default.addStore(name, new StoreClient_1.StoreClient(config.data ?? {}));
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ApiClient } from "../api/ApiClient";
|
|
2
|
+
import { StoreClient, StoreData } from "./StoreClient";
|
|
3
|
+
export type storesConfig = {
|
|
4
|
+
appData?: StoreData;
|
|
5
|
+
apiData?: StoreData;
|
|
6
|
+
stores: Record<string, ApiClient | StoreClient>;
|
|
7
|
+
};
|
|
8
|
+
export declare function setStores({ stores, appData, apiData }: storesConfig): void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setStores = setStores;
|
|
7
|
+
const ApiClient_1 = require("../api/ApiClient");
|
|
8
|
+
const StoreClient_1 = require("./StoreClient");
|
|
9
|
+
const StoreManager_1 = __importDefault(require("./StoreManager"));
|
|
10
|
+
function setStores({ stores, appData, apiData }) {
|
|
11
|
+
// Agrega los stores pasados
|
|
12
|
+
Object.entries(stores)
|
|
13
|
+
.forEach(([model, store]) => {
|
|
14
|
+
StoreManager_1.default.addStore(model, store);
|
|
15
|
+
});
|
|
16
|
+
// Inicializa la info del app y api stores
|
|
17
|
+
if (appData) {
|
|
18
|
+
StoreManager_1.default.addStore("app", new StoreClient_1.StoreClient(appData));
|
|
19
|
+
}
|
|
20
|
+
if (apiData) {
|
|
21
|
+
StoreManager_1.default.addStore("api", new ApiClient_1.ApiClient(apiData));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Component = void 0;
|
|
37
|
+
class Component {
|
|
38
|
+
method = () => { };
|
|
39
|
+
// Importa dinámicamente el archivo especificado en path
|
|
40
|
+
constructor(path) {
|
|
41
|
+
Promise.resolve(`${path}`).then(s => __importStar(require(s))).then((module) => {
|
|
42
|
+
this.method = module.default;
|
|
43
|
+
})
|
|
44
|
+
.catch((error) => {
|
|
45
|
+
throw `Error importing module at ${path}: ${error}`;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.Component = Component;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { usePathname, useRouter } from "next/navigation";
|
|
3
|
+
import { ApiClient } from "../api/ApiClient";
|
|
4
|
+
import { StoreClient } from "../store/StoreClient";
|
|
5
|
+
export declare abstract class Controller<Params = {}> {
|
|
6
|
+
router: ReturnType<typeof useRouter>;
|
|
7
|
+
pathname: ReturnType<typeof usePathname>;
|
|
8
|
+
params: Params & {
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
};
|
|
11
|
+
store: Record<string, ApiClient | StoreClient>;
|
|
12
|
+
onInit(): void;
|
|
13
|
+
onRender(): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
type ResolvedStore = {
|
|
3
|
+
type: "local";
|
|
4
|
+
data: Record<string, any>;
|
|
5
|
+
} | {
|
|
6
|
+
type: "api";
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
};
|
|
9
|
+
type StoreProviderProps = {
|
|
10
|
+
stores: Record<string, ResolvedStore>;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Inicializa los stores definidos en nano-app.config.ts (resueltos por el plugin)
|
|
15
|
+
* y los registra en el StoreManager. Debe envolver toda la app en el layout raíz.
|
|
16
|
+
*/
|
|
17
|
+
export declare function StoreProvider({ stores, children }: StoreProviderProps): React.JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.StoreProvider = StoreProvider;
|
|
40
|
+
const react_1 = __importStar(require("react"));
|
|
41
|
+
const ApiClient_1 = require("../api/ApiClient");
|
|
42
|
+
const StoreClient_1 = require("../store/StoreClient");
|
|
43
|
+
const StoreManager_1 = __importDefault(require("../store/StoreManager"));
|
|
44
|
+
/**
|
|
45
|
+
* Inicializa los stores definidos en nano-app.config.ts (resueltos por el plugin)
|
|
46
|
+
* y los registra en el StoreManager. Debe envolver toda la app en el layout raíz.
|
|
47
|
+
*/
|
|
48
|
+
function StoreProvider({ stores, children }) {
|
|
49
|
+
const initialized = (0, react_1.useRef)(false);
|
|
50
|
+
if (!initialized.current) {
|
|
51
|
+
initialized.current = true;
|
|
52
|
+
Object.entries(stores).forEach(([name, entry]) => {
|
|
53
|
+
if (entry.type === "api") {
|
|
54
|
+
StoreManager_1.default.addStore(name, new ApiClient_1.ApiClient(entry.baseUrl ?? "/api"));
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
StoreManager_1.default.addStore(name, new StoreClient_1.StoreClient(entry.data ?? {}));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return react_1.default.createElement(react_1.default.Fragment, null, children);
|
|
62
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Controller } from "./Controller";
|
|
2
|
+
export type ControllerClass<C extends Controller> = new () => C;
|
|
3
|
+
export type ViewFn<C extends Controller> = (controller: C) => React.ReactNode;
|
|
4
|
+
/**
|
|
5
|
+
* Crea un componente React uniendo stores, controller y view.
|
|
6
|
+
*
|
|
7
|
+
* @param stores - Nombres de los stores a los que se suscribe el componente.
|
|
8
|
+
* @param ControllerClass - Clase del controller (se instancia una vez al montar).
|
|
9
|
+
* @param view - Función que recibe la instancia del controller y retorna JSX.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createComponent<C extends Controller>(stores: string[], ControllerClass: ControllerClass<C>, view: ViewFn<C>): (params?: Record<string, any>) => React.ReactNode;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createComponent = createComponent;
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
const navigation_1 = require("next/navigation");
|
|
9
|
+
const StoreManager_1 = __importDefault(require("../store/StoreManager"));
|
|
10
|
+
/**
|
|
11
|
+
* Crea un componente React uniendo stores, controller y view.
|
|
12
|
+
*
|
|
13
|
+
* @param stores - Nombres de los stores a los que se suscribe el componente.
|
|
14
|
+
* @param ControllerClass - Clase del controller (se instancia una vez al montar).
|
|
15
|
+
* @param view - Función que recibe la instancia del controller y retorna JSX.
|
|
16
|
+
*/
|
|
17
|
+
function createComponent(stores, ControllerClass, view) {
|
|
18
|
+
return function (params = {}) {
|
|
19
|
+
// Suscribe reactivamente a cada store declarado — Zustand re-renderiza cuando cambian
|
|
20
|
+
stores.forEach((name) => StoreManager_1.default.getStore(name)?.use());
|
|
21
|
+
// Instancia el controller una sola vez por montaje
|
|
22
|
+
const ref = (0, react_1.useRef)(null);
|
|
23
|
+
if (!ref.current) {
|
|
24
|
+
ref.current = new ControllerClass();
|
|
25
|
+
ref.current.onInit();
|
|
26
|
+
}
|
|
27
|
+
const c = ref.current;
|
|
28
|
+
// Inyecta hooks de Next.js y params en cada render
|
|
29
|
+
c.router = (0, navigation_1.useRouter)();
|
|
30
|
+
c.pathname = (0, navigation_1.usePathname)();
|
|
31
|
+
c.params = params;
|
|
32
|
+
// Inyecta el mapa de stores en cada render para que los métodos lean valores frescos
|
|
33
|
+
stores.forEach((name) => {
|
|
34
|
+
c.store[name] = StoreManager_1.default.getStore(name);
|
|
35
|
+
});
|
|
36
|
+
// Ejecuta onRender en cada render
|
|
37
|
+
c.onRender();
|
|
38
|
+
return view(c);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ApiClient } from "../api/ApiClient";
|
|
2
|
+
import { StoreClient } from "../store/StoreClient";
|
|
3
|
+
export type ControllerStores = Record<string, ApiClient | StoreClient>;
|
|
4
|
+
/** Contexto disponible en onInit, onRender y todos los métodos */
|
|
5
|
+
export type ControllerContext = {
|
|
6
|
+
store: ControllerStores;
|
|
7
|
+
params: Record<string, any>;
|
|
8
|
+
};
|
|
9
|
+
export type ControllerSetup<T extends Record<string, any>> = (ctx: ControllerContext) => T;
|
|
10
|
+
export type ControllerDescriptor<T extends Record<string, any>> = {
|
|
11
|
+
_isDescriptor: true;
|
|
12
|
+
stores: string[];
|
|
13
|
+
setup: ControllerSetup<T>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Define la lógica de un componente.
|
|
17
|
+
*
|
|
18
|
+
* @param stores - Nombres de los stores a los que se suscribe el componente.
|
|
19
|
+
* @param setup - Función que recibe el contexto (store, params) y retorna
|
|
20
|
+
* un objeto con propiedades y métodos expuestos a la view.
|
|
21
|
+
* Se ejecuta una sola vez al montar el componente.
|
|
22
|
+
*/
|
|
23
|
+
export declare function defineController<T extends Record<string, any>>(stores: string[], setup: ControllerSetup<T>): ControllerDescriptor<T>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defineController = defineController;
|
|
4
|
+
/**
|
|
5
|
+
* Define la lógica de un componente.
|
|
6
|
+
*
|
|
7
|
+
* @param stores - Nombres de los stores a los que se suscribe el componente.
|
|
8
|
+
* @param setup - Función que recibe el contexto (store, params) y retorna
|
|
9
|
+
* un objeto con propiedades y métodos expuestos a la view.
|
|
10
|
+
* Se ejecuta una sola vez al montar el componente.
|
|
11
|
+
*/
|
|
12
|
+
function defineController(stores, setup) {
|
|
13
|
+
return { _isDescriptor: true, stores, setup };
|
|
14
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Controller } from "./Controller";
|
|
2
|
+
export declare const defineView: (controller: Controller, view: (controller: InstanceType<typeof Controller>) => React.ReactNode) => Promise<() => import("react").ReactNode>;
|
|
3
|
+
export declare const defineComponent: (controllerPath: string, viewPath: string) => Promise<() => any>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.defineComponent = exports.defineView = void 0;
|
|
37
|
+
const defineView = async (controller, view) => () => view(controller);
|
|
38
|
+
exports.defineView = defineView;
|
|
39
|
+
const defineComponent = async (controllerPath, viewPath) => {
|
|
40
|
+
const controller = (new (await Promise.resolve(`${controllerPath}`).then(s => __importStar(require(s)))).default)();
|
|
41
|
+
const view = await Promise.resolve(`${viewPath}`).then(s => __importStar(require(s)));
|
|
42
|
+
return () => view(controller);
|
|
43
|
+
};
|
|
44
|
+
exports.defineComponent = defineComponent;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const defineComponent: (componentName: string) => Promise<string>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.defineComponent = void 0;
|
|
40
|
+
const path_1 = __importDefault(require("path"));
|
|
41
|
+
const controllersPath = path_1.default.join(process.cwd(), "src", "controllers");
|
|
42
|
+
const viewsPath = path_1.default.join(process.cwd(), "src", "views");
|
|
43
|
+
const defineComponent = async (componentName) => {
|
|
44
|
+
try {
|
|
45
|
+
const [ControllerModule, ViewModule] = await Promise.all([
|
|
46
|
+
Promise.resolve(`${path_1.default.join(controllersPath, componentName)}`).then(s => __importStar(require(s))),
|
|
47
|
+
Promise.resolve(`${path_1.default.join(viewsPath, componentName)}`).then(s => __importStar(require(s))),
|
|
48
|
+
]);
|
|
49
|
+
const controller = new ControllerModule.default();
|
|
50
|
+
// console.log(ViewModule.default?.(controller))
|
|
51
|
+
// ViewModule.default?.(controller) ?? null;
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error(`[defineComponent] Error loading "${componentName}":`, error);
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
exports.defineComponent = defineComponent;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ApiResponse } from "@nano-app/backend";
|
|
2
|
+
/**
|
|
3
|
+
* Toast: implementación por defecto no-op.
|
|
4
|
+
* La app puede reasignar los métodos (ej. Toast.error = (m) => addToast(...))
|
|
5
|
+
* para mostrar notificaciones con su UI.
|
|
6
|
+
*/
|
|
7
|
+
export declare class Toast {
|
|
8
|
+
static error(_message: string): void;
|
|
9
|
+
static success(_message: string): void;
|
|
10
|
+
static warning(_message: string): void;
|
|
11
|
+
static apiResponse<T>(response: ApiResponse<T>): ApiResponse<T>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Toast = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Toast: implementación por defecto no-op.
|
|
6
|
+
* La app puede reasignar los métodos (ej. Toast.error = (m) => addToast(...))
|
|
7
|
+
* para mostrar notificaciones con su UI.
|
|
8
|
+
*/
|
|
9
|
+
class Toast {
|
|
10
|
+
static error(_message) { }
|
|
11
|
+
static success(_message) { }
|
|
12
|
+
static warning(_message) { }
|
|
13
|
+
static apiResponse(response) {
|
|
14
|
+
if (response.message) {
|
|
15
|
+
if (response.warning) {
|
|
16
|
+
Toast.warning(response.message);
|
|
17
|
+
}
|
|
18
|
+
else if (response.success) {
|
|
19
|
+
Toast.success(response.message);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
Toast.error(response.message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return response;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.Toast = Toast;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TokenHelper = void 0;
|
|
4
|
+
class TokenHelper {
|
|
5
|
+
static TOKEN_KEY = "app_token";
|
|
6
|
+
static set(token) {
|
|
7
|
+
if (typeof localStorage !== "undefined") {
|
|
8
|
+
localStorage.setItem(this.TOKEN_KEY, token);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
static get() {
|
|
12
|
+
if (typeof localStorage === "undefined")
|
|
13
|
+
return "";
|
|
14
|
+
return localStorage.getItem(this.TOKEN_KEY) ?? "";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.TokenHelper = TokenHelper;
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nano-app/frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "View + Controller frontend architecture for React/Next.js",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"require": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./plugin": {
|
|
13
|
+
"require": "./dist/plugin/index.js",
|
|
14
|
+
"types": "./dist/plugin/index.d.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": ["dist"],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["architecture", "typescript", "react", "next", "controller", "view"],
|
|
23
|
+
"author": "Cristian Serrano",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/Nano-Nathan/nano-app.git"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"next": ">=14.0.0",
|
|
31
|
+
"react": ">=18.0.0",
|
|
32
|
+
"react-dom": ">=18.0.0",
|
|
33
|
+
"zustand": ">=5.0.0",
|
|
34
|
+
"@nano-app/backend": "file:../backend"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"jiti": "^1.21.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^20.0.0",
|
|
41
|
+
"@types/react": "^18.0.0",
|
|
42
|
+
"next": "^14.0.0",
|
|
43
|
+
"react": "^18.0.0",
|
|
44
|
+
"react-dom": "^18.0.0",
|
|
45
|
+
"typescript": "^5.3.0",
|
|
46
|
+
"zustand": "^5.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|