hiuhu-table-generator 1.0.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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../src/commands/gen-datatable.js";
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "hiuhu-table-generator",
3
+ "version": "1.0.0",
4
+ "description": "CLI generator for table feature in Next.js",
5
+ "type": "module",
6
+ "bin": {
7
+ "gen-datatable": "./bin/gen-datatable.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src"
12
+ ],
13
+ "keywords": [
14
+ "hiuhu",
15
+ "generator",
16
+ "table",
17
+ "cli"
18
+ ],
19
+ "author": "HiuHu",
20
+ "license": "MIT"
21
+ }
@@ -0,0 +1,222 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ import { injectAfter } from "../utils/injectAfter.js";
6
+ import { loadGeneratorConfig } from "../utils/loadGeneratorConfig.js";
7
+ import {
8
+ toCamelCase,
9
+ toPascalCase,
10
+ toSnakeCase,
11
+ removeExtension,
12
+ } from "../utils/formatter.js";
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
17
+ /* =======================
18
+ LOAD & VALIDATE CONFIG
19
+ ======================= */
20
+ let config;
21
+ try {
22
+ config = loadGeneratorConfig();
23
+ } catch (err) {
24
+ console.error(err.message);
25
+ process.exit(1);
26
+ }
27
+
28
+ const {
29
+ sharedTypes,
30
+ sharedTypesAlias,
31
+ features,
32
+ featuresAlias,
33
+ redux,
34
+ reduxAlias,
35
+ reduxStore,
36
+ reduxHook,
37
+ } = config.paths;
38
+ const {
39
+ components: { table, columnHeader },
40
+ types: { state, redux: typesRedux },
41
+ } = config.datatable;
42
+
43
+ const datatableImports = {
44
+ table: buildImport(table),
45
+ columnHeader: buildImport(columnHeader),
46
+ state: buildImport(state),
47
+ redux: buildImport(typesRedux),
48
+ };
49
+
50
+ const REDUX_STORE_FILE_PATH = path.resolve(process.cwd(), redux, reduxStore);
51
+ const REDUX_HOOK_FILE_ALIAS = `${reduxAlias}/${reduxHook}`;
52
+
53
+ const rawFeature = process.argv[2];
54
+ const rawTableKey = process.argv[3];
55
+ const tableKeyPascal = toPascalCase(rawTableKey);
56
+ const tableKeySnake = toSnakeCase(rawTableKey);
57
+ const feature = toSnakeCase(rawFeature);
58
+ const Feature = toPascalCase(feature + tableKeyPascal);
59
+ const featureVar = toCamelCase(feature + tableKeyPascal);
60
+ const endpoint = process.argv[4];
61
+ const typeName = process.argv[5];
62
+
63
+ if (!feature || !tableKeySnake || !endpoint || !typeName) {
64
+ console.error(
65
+ "Usage: gen:datatable <feature> <tableKey> <endpoint> <typeName>",
66
+ );
67
+ process.exit(1);
68
+ }
69
+
70
+ const TYPE_BASE_PATH = path.resolve(process.cwd(), sharedTypes);
71
+
72
+ const possibleTypePaths = [
73
+ path.join(TYPE_BASE_PATH, `${typeName}.ts`),
74
+ path.join(TYPE_BASE_PATH, `${typeName.toLowerCase()}.ts`),
75
+ ];
76
+
77
+ const typeExists = possibleTypePaths.some(fs.existsSync);
78
+
79
+ if (!typeExists) {
80
+ console.error(`❌ Type "${typeName}" not found in ${sharedTypes}`);
81
+ process.exit(1);
82
+ }
83
+
84
+ const replace = (tpl) =>
85
+ tpl
86
+ .replace(/{{datatableTableImport}}/g, datatableImports.table)
87
+ .replace(/{{datatableTable}}/g, table.component)
88
+ .replace(/{{datatableColumnHeaderImport}}/g, datatableImports.columnHeader)
89
+ .replace(/{{datatableColumnHeader}}/g, columnHeader.component)
90
+ .replace(/{{datatableStateImport}}/g, datatableImports.state)
91
+ .replace(/{{datatableState}}/g, state.name)
92
+ .replace(/{{datatableReduxImport}}/g, datatableImports.redux)
93
+ .replace(/{{datatableRedux}}/g, typesRedux.name)
94
+ .replace(/{{feature}}/g, feature)
95
+ .replace(/{{Feature}}/g, Feature)
96
+ .replace(/{{tablekey}}/g, tableKeySnake)
97
+ .replace(/{{featureVar}}/g, featureVar)
98
+ .replace(/{{Type}}/g, typeName)
99
+ .replace(/{{sharedTypesAlias}}/g, sharedTypesAlias)
100
+ .replace(/{{reduxHookFileAlias}}/g, removeExtension(REDUX_HOOK_FILE_ALIAS))
101
+ .replace(/{{endpoint}}/g, endpoint);
102
+
103
+ const basePath = `${features}/${feature}`;
104
+
105
+ const files = {
106
+ "components/DataFeature.tsx": "DataFeature.tsx.tpl",
107
+ "components/columnsFeature.tsx": "columnsFeature.tsx.tpl",
108
+ "hooks/table/useDataTableFeature.ts": "useDataTableFeature.ts.tpl",
109
+ "hooks/query/useGetFeature.ts": "useGetFeature.ts.tpl",
110
+ "services/feature.service.ts": "feature.service.ts.tpl",
111
+ "slices/feature-__tablekey__-datatable.slice.ts":
112
+ "feature-datatable.slice.ts.tpl",
113
+ "slices/index.ts": "slice-index.ts.tpl",
114
+ };
115
+
116
+ const reducerKey = `${featureVar}Datatable`;
117
+ const STORE_PATH = path.resolve(REDUX_STORE_FILE_PATH);
118
+ if (!fs.existsSync(STORE_PATH)) {
119
+ console.error("❌ Redux store file not found");
120
+ process.exit(1);
121
+ }
122
+ injectAfter({
123
+ filePath: STORE_PATH,
124
+ marker: "// AUTO-GENERATED IMPORTS (DO NOT REMOVE)",
125
+ content: `import { ${featureVar}DatatableReducer } from "${featuresAlias}/${feature}/slices";`,
126
+ skipIfExists: `import { ${featureVar}DatatableReducer } from "${featuresAlias}/${feature}/slices";`,
127
+ });
128
+
129
+ injectAfter({
130
+ filePath: STORE_PATH,
131
+ marker: "// AUTO-GENERATED REDUCERS (DO NOT REMOVE)",
132
+ content: ` ${reducerKey}: ${featureVar}DatatableReducer,`,
133
+ skipIfExists: ` ${reducerKey}: ${featureVar}DatatableReducer,`,
134
+ });
135
+
136
+ injectAfter({
137
+ filePath: STORE_PATH,
138
+ marker: "// AUTO-GENERATED PERSIST (DO NOT REMOVE)",
139
+ content: ` "${reducerKey}",`,
140
+ skipIfExists: ` "${reducerKey}",`,
141
+ });
142
+
143
+ Object.entries(files).forEach(([target, template]) => {
144
+ const filePath = path.join(
145
+ basePath,
146
+ target
147
+ .replace("Feature", Feature)
148
+ .replace("feature", feature)
149
+ .replace("__tablekey__", tableKeySnake),
150
+ );
151
+
152
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
153
+
154
+ const tpl = fs.readFileSync(
155
+ path.join(__dirname, "templates", template),
156
+ "utf-8",
157
+ );
158
+
159
+ /* ======================
160
+ SERVICE FILE
161
+ ====================== */
162
+ if (target === "services/feature.service.ts") {
163
+ if (!fs.existsSync(filePath)) {
164
+ fs.writeFileSync(filePath, replace(tpl));
165
+ } else {
166
+ injectAfter({
167
+ filePath,
168
+ marker: "// AUTO-GENERATED SERVICE FUNCTIONS (DO NOT REMOVE)",
169
+ content: replace(
170
+ `export const get{{Feature}} = ({ signal }: QueryFunctionContext) =>
171
+ apiGet<{{Type}}[]>("{{endpoint}}", { signal });`,
172
+ ),
173
+ skipIfExists: `get${Feature}`,
174
+ });
175
+
176
+ injectAfter({
177
+ filePath,
178
+ marker: "// AUTO-GENERATED SERVICE IMPORT (DO NOT REMOVE)",
179
+ content: replace(
180
+ `import { {{Type}} } from "{{sharedTypesAlias}}/{{Type}}";`,
181
+ ),
182
+ skipIfExists: `import { ${typeName} } from "${sharedTypesAlias}/${typeName}`,
183
+ });
184
+ }
185
+ return;
186
+ }
187
+
188
+ /* ======================
189
+ SLICE INDEX
190
+ ====================== */
191
+ if (target === "slices/index.ts") {
192
+ if (!fs.existsSync(filePath)) {
193
+ fs.writeFileSync(filePath, replace(tpl));
194
+ } else {
195
+ injectAfter({
196
+ filePath,
197
+ marker: "// AUTO-GENERATED SLICE EXPORTS (DO NOT REMOVE)",
198
+ content:
199
+ replace(`export { default as {{featureVar}}DatatableReducer } from "./{{feature}}-{{tablekey}}-datatable.slice";
200
+ export * from "./{{feature}}-{{tablekey}}-datatable.slice";`),
201
+ skipIfExists: `export { default as ${featureVar}DatatableReducer } from "./${feature}-${tableKeySnake}-datatable.slice";
202
+ export * from "./${feature}-${tableKeySnake}-datatable.slice";`,
203
+ });
204
+ }
205
+ return;
206
+ }
207
+
208
+ /* ======================
209
+ DEFAULT (STRICT)
210
+ ====================== */
211
+ if (fs.existsSync(filePath)) {
212
+ console.error(`❌ File already exists: ${filePath}`);
213
+ process.exit(1);
214
+ }
215
+
216
+ fs.writeFileSync(filePath, replace(tpl));
217
+ });
218
+
219
+ function buildImport({ component, name, importPath }) {
220
+ const symbol = component || name;
221
+ return `import { ${symbol} } from "${importPath}";`;
222
+ }
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { useMemo } from "react";
3
+ {{datatableTableImport}}
4
+ import { create{{Feature}}Columns } from "./columns{{Feature}}";
5
+ import { useGet{{Feature}} } from "../hooks/query/useGet{{Feature}}";
6
+ import { useDataTable{{Feature}} } from "../hooks/table/useDataTable{{Feature}}";
7
+
8
+ const Data{{Feature}} = () => {
9
+ const { data, isLoading } = useGet{{Feature}}();
10
+
11
+ const tabel = useDataTable{{Feature}}();
12
+
13
+ const columns = useMemo(() => create{{Feature}}Columns(), []);
14
+
15
+ return (
16
+ <{{datatableTable}}
17
+ {...tabel}
18
+ columns={columns}
19
+ data={data}
20
+ isLoading={isLoading}
21
+ />
22
+ );
23
+ };
24
+
25
+ export default Data{{Feature}};
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ {{datatableColumnHeaderImport}}
4
+ import { ColumnDef } from "@tanstack/react-table";
5
+ import { {{Type}} } from "{{sharedTypesAlias}}/{{Type}}";
6
+
7
+ export const create{{Feature}}Columns = (): ColumnDef<{{Type}}>[] => [
8
+ {
9
+ accessorKey: "rowNumber",
10
+ id: "nomor_urut",
11
+ header: ({ column }) => <{{datatableColumnHeader}} column={column} title="#" />,
12
+ cell: ({ row, table }) => {
13
+ const filteredRows = table.getRowModel().rows;
14
+ const rowIndex = filteredRows.findIndex((r) => r.id === row.id);
15
+ const { pageIndex, pageSize } = table.getState().pagination;
16
+ const rowNumber = pageIndex * pageSize + rowIndex + 1;
17
+ return <div>{rowNumber}</div>;
18
+ },
19
+ size: 50,
20
+ enableHiding: true,
21
+ enableColumnFilter: false,
22
+ enableGlobalFilter: false,
23
+ enablePinning: false,
24
+ enableSorting: false,
25
+ enableGrouping: false,
26
+ enableMultiSort: false,
27
+ enableResizing: false,
28
+ },
29
+ ];
@@ -0,0 +1,63 @@
1
+ {{datatableStateImport}}
2
+ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
3
+ import {
4
+ ColumnFiltersState,
5
+ SortingState,
6
+ RowSelectionState,
7
+ VisibilityState,
8
+ PaginationState,
9
+ ColumnPinningState,
10
+ } from "@tanstack/react-table";
11
+
12
+ const initialState: {{datatableState}} = {
13
+ pagination: { pageIndex: 0, pageSize: 10 },
14
+ sorting: [],
15
+ columnFilters: [],
16
+ globalFilter: "",
17
+ columnVisibility: {},
18
+ rowSelection: {},
19
+ columnPinning: {
20
+ left: ["select", "nomor_urut"],
21
+ right: ["actions"],
22
+ },
23
+ };
24
+
25
+ const {{featureVar}}DataTableSlice = createSlice({
26
+ name: "{{featureVar}}Datatable",
27
+ initialState,
28
+ reducers: {
29
+ setPagination{{Feature}}(state, action: PayloadAction<PaginationState>) {
30
+ state.pagination = action.payload;
31
+ },
32
+ setSorting{{Feature}}(state, action: PayloadAction<SortingState>) {
33
+ state.sorting = action.payload;
34
+ },
35
+ setColumnFilters{{Feature}}(state, action: PayloadAction<ColumnFiltersState>) {
36
+ state.columnFilters = action.payload;
37
+ },
38
+ setGlobalFilter{{Feature}}(state, action: PayloadAction<string>) {
39
+ state.globalFilter = action.payload;
40
+ },
41
+ setColumnVisibility{{Feature}}(state, action: PayloadAction<VisibilityState>) {
42
+ state.columnVisibility = action.payload;
43
+ },
44
+ setRowSelection{{Feature}}(state, action: PayloadAction<RowSelectionState>) {
45
+ state.rowSelection = action.payload;
46
+ },
47
+ setColumnPinning{{Feature}}(state, action: PayloadAction<ColumnPinningState>) {
48
+ state.columnPinning = action.payload;
49
+ },
50
+ },
51
+ });
52
+
53
+ export const {
54
+ setPagination{{Feature}},
55
+ setSorting{{Feature}},
56
+ setColumnFilters{{Feature}},
57
+ setGlobalFilter{{Feature}},
58
+ setColumnVisibility{{Feature}},
59
+ setRowSelection{{Feature}},
60
+ setColumnPinning{{Feature}},
61
+ } = {{featureVar}}DataTableSlice.actions;
62
+
63
+ export default {{featureVar}}DataTableSlice.reducer;
@@ -0,0 +1,9 @@
1
+ import { apiGet } from "@/frontend/lib/apiClient";
2
+ import { QueryFunctionContext } from "@tanstack/react-query";
3
+ // AUTO-GENERATED SERVICE IMPORT (DO NOT REMOVE)
4
+ import { {{Type}} } from "{{sharedTypesAlias}}/{{Type}}";
5
+
6
+ export const get{{Feature}} = ({ signal }: QueryFunctionContext) =>
7
+ apiGet<{{Type}}[]>("{{endpoint}}", { signal });
8
+
9
+ // AUTO-GENERATED SERVICE FUNCTIONS (DO NOT REMOVE)
@@ -0,0 +1,3 @@
1
+ // AUTO-GENERATED SLICE EXPORTS (DO NOT REMOVE)
2
+ export { default as {{featureVar}}DatatableReducer } from "./{{feature}}-{{tablekey}}-datatable.slice";
3
+ export * from "./{{feature}}-{{tablekey}}-datatable.slice";
@@ -0,0 +1,72 @@
1
+ import {
2
+ ColumnFiltersState,
3
+ functionalUpdate,
4
+ SortingState,
5
+ Updater,
6
+ RowSelectionState,
7
+ VisibilityState,
8
+ PaginationState,
9
+ ColumnPinningState,
10
+ } from "@tanstack/react-table";
11
+ import {
12
+ setColumnFilters{{Feature}},
13
+ setColumnPinning{{Feature}},
14
+ setColumnVisibility{{Feature}},
15
+ setGlobalFilter{{Feature}},
16
+ setPagination{{Feature}},
17
+ setRowSelection{{Feature}},
18
+ setSorting{{Feature}},
19
+ } from "../../slices";
20
+ {{datatableReduxImport}}
21
+ import { useAppDispatch, useAppSelector } from "{{reduxHookFileAlias}}";
22
+
23
+ export function useDataTable{{Feature}}(): {{datatableRedux}} {
24
+ const dispatch = useAppDispatch();
25
+ const {
26
+ pagination,
27
+ sorting,
28
+ columnFilters,
29
+ globalFilter,
30
+ columnVisibility,
31
+ rowSelection,
32
+ columnPinning,
33
+ } = useAppSelector((state) => state.{{featureVar}}Datatable);
34
+
35
+ return {
36
+ pagination,
37
+ sorting,
38
+ columnFilters,
39
+ globalFilter,
40
+ columnVisibility,
41
+ rowSelection,
42
+ columnPinning,
43
+
44
+ onPaginationChange: (updater: Updater<PaginationState>) => {
45
+ const nextPagination = functionalUpdate(updater, pagination);
46
+ dispatch(setPagination{{Feature}}(nextPagination));
47
+ },
48
+ onSortingChange: (updater: Updater<SortingState>) => {
49
+ const nextSorting = functionalUpdate(updater, sorting);
50
+ dispatch(setSorting{{Feature}}(nextSorting));
51
+ },
52
+ onColumnFiltersChange: (updater: Updater<ColumnFiltersState>) => {
53
+ const nextFilters = functionalUpdate(updater, columnFilters);
54
+ dispatch(setColumnFilters{{Feature}}(nextFilters));
55
+ },
56
+ onGlobalFilterChange: (value: string) => {
57
+ dispatch(setGlobalFilter{{Feature}}(value));
58
+ },
59
+ onColumnVisibilityChange: (updater: Updater<VisibilityState>) => {
60
+ const nextVisibility = functionalUpdate(updater, columnVisibility);
61
+ dispatch(setColumnVisibility{{Feature}}(nextVisibility));
62
+ },
63
+ onRowSelectionChange: (updater: Updater<RowSelectionState>) => {
64
+ const nextSelection = functionalUpdate(updater, rowSelection);
65
+ dispatch(setRowSelection{{Feature}}(nextSelection));
66
+ },
67
+ onColumnPinningChange: (updater: Updater<ColumnPinningState>) => {
68
+ const nextPinning = functionalUpdate(updater, columnPinning);
69
+ dispatch(setColumnPinning{{Feature}}(nextPinning));
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,9 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { get{{Feature}} } from "../../services/{{feature}}.service";
3
+
4
+ export function useGet{{Feature}}() {
5
+ return useQuery({
6
+ queryKey: ["{{feature}}_{{tablekey}}"],
7
+ queryFn: get{{Feature}},
8
+ });
9
+ }
@@ -0,0 +1,22 @@
1
+ export function toSnakeCase(str) {
2
+ return str
3
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
4
+ .replace(/[\s-]+/g, "_")
5
+ .toLowerCase();
6
+ }
7
+
8
+ export function toPascalCase(str) {
9
+ return str
10
+ .replace(/[_\-\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
11
+ .replace(/^(.)/, (m) => m.toUpperCase());
12
+ }
13
+
14
+ export function toCamelCase(str) {
15
+ const pascal = toPascalCase(str);
16
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
17
+ }
18
+
19
+ export function removeExtension(filename) {
20
+ const lastDot = filename.lastIndexOf(".");
21
+ return lastDot === -1 ? filename : filename.substring(0, lastDot);
22
+ }
@@ -0,0 +1,17 @@
1
+ import fs from "fs";
2
+
3
+ export function injectAfter({ filePath, marker, content, skipIfExists }) {
4
+ const source = fs.readFileSync(filePath, "utf-8");
5
+
6
+ if (skipIfExists && source.includes(skipIfExists)) {
7
+ return; // aman, idempotent
8
+ }
9
+
10
+ if (!source.includes(marker)) {
11
+ throw new Error(`Marker not found in ${filePath}`);
12
+ }
13
+
14
+ const updated = source.replace(marker, `${marker}\n${content}`);
15
+
16
+ fs.writeFileSync(filePath, updated);
17
+ }
@@ -0,0 +1,97 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { validateDatatable } from "./validateDatatable.js";
4
+
5
+ function assert(condition, message) {
6
+ if (!condition) {
7
+ throw new Error(message);
8
+ }
9
+ }
10
+
11
+ function hasExtension(filename) {
12
+ return path.extname(filename).length > 0;
13
+ }
14
+
15
+ export function loadGeneratorConfig() {
16
+ const CONFIG_PATH = path.resolve(process.cwd(), "generator.config.json");
17
+
18
+ assert(fs.existsSync(CONFIG_PATH), "❌ generator.config.json not found");
19
+
20
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
21
+ const paths = config.paths;
22
+
23
+ assert(paths, "❌ paths is missing in generator.config.json");
24
+
25
+ const {
26
+ sharedTypes,
27
+ sharedTypesAlias,
28
+ features,
29
+ featuresAlias,
30
+ redux,
31
+ reduxAlias,
32
+ reduxStore,
33
+ reduxHook,
34
+ } = paths;
35
+
36
+ // 1️⃣ Required paths (HARUS ADA SEMUA)
37
+ const required = {
38
+ sharedTypes,
39
+ sharedTypesAlias,
40
+ features,
41
+ featuresAlias,
42
+ redux,
43
+ reduxAlias,
44
+ reduxStore,
45
+ reduxHook,
46
+ };
47
+
48
+ for (const [key, value] of Object.entries(required)) {
49
+ assert(
50
+ typeof value === "string" && value.trim().length > 0,
51
+ `❌ paths.${key} must be defined`
52
+ );
53
+ }
54
+
55
+ // 2️⃣ File name HARUS ada extension
56
+ assert(
57
+ hasExtension(reduxStore),
58
+ "❌ paths.reduxStore must include file extension (e.g. store.ts)"
59
+ );
60
+
61
+ assert(
62
+ hasExtension(reduxHook),
63
+ "❌ paths.reduxHook must include file extension (e.g. hooks.ts)"
64
+ );
65
+
66
+ const datatable = config.datatable;
67
+
68
+ validateDatatable(datatable);
69
+
70
+ // 3️⃣ File HARUS exist
71
+ const reduxStorePath = path.resolve(process.cwd(), redux, reduxStore);
72
+ const reduxHookPath = path.resolve(process.cwd(), redux, reduxHook);
73
+
74
+ assert(
75
+ fs.existsSync(reduxStorePath),
76
+ `❌ Redux store file not found: ${reduxStorePath}`
77
+ );
78
+
79
+ assert(
80
+ fs.existsSync(reduxHookPath),
81
+ `❌ Redux hook file not found: ${reduxHookPath}`
82
+ );
83
+
84
+ return {
85
+ paths: {
86
+ sharedTypes,
87
+ sharedTypesAlias,
88
+ features,
89
+ featuresAlias,
90
+ redux,
91
+ reduxAlias,
92
+ reduxStore,
93
+ reduxHook,
94
+ },
95
+ datatable,
96
+ };
97
+ }
@@ -0,0 +1,71 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ function validateEntry(entry, label, requiredKeys) {
5
+ if (!entry) {
6
+ throw new Error(`❌ datatable.${label} is required`);
7
+ }
8
+
9
+ for (const key of requiredKeys) {
10
+ if (!entry[key]) {
11
+ throw new Error(`❌ datatable.${label}.${key} is required`);
12
+ }
13
+ }
14
+
15
+ const absPath = path.resolve(process.cwd(), entry.filePath);
16
+
17
+ if (!fs.existsSync(absPath)) {
18
+ throw new Error(`❌ File not found: ${entry.filePath}`);
19
+ }
20
+
21
+ const content = fs.readFileSync(absPath, "utf-8");
22
+
23
+ const exportName = entry.component || entry.name;
24
+ if (exportName && !content.includes(exportName)) {
25
+ throw new Error(`❌ "${exportName}" is not exported in ${entry.filePath}`);
26
+ }
27
+ }
28
+
29
+ export function validateDatatable(datatable) {
30
+ if (!datatable) {
31
+ throw new Error("❌ datatable config is required");
32
+ }
33
+
34
+ /* =======================
35
+ COMPONENTS
36
+ ======================= */
37
+ if (!datatable.components) {
38
+ throw new Error("❌ datatable.components is required");
39
+ }
40
+
41
+ validateEntry(datatable.components.table, "components.table", [
42
+ "component",
43
+ "importPath",
44
+ "filePath",
45
+ ]);
46
+
47
+ validateEntry(datatable.components.columnHeader, "components.columnHeader", [
48
+ "component",
49
+ "importPath",
50
+ "filePath",
51
+ ]);
52
+
53
+ /* =======================
54
+ TYPES
55
+ ======================= */
56
+ if (!datatable.types) {
57
+ throw new Error("❌ datatable.types is required");
58
+ }
59
+
60
+ validateEntry(datatable.types.state, "types.state", [
61
+ "name",
62
+ "importPath",
63
+ "filePath",
64
+ ]);
65
+
66
+ validateEntry(datatable.types.redux, "types.redux", [
67
+ "name",
68
+ "importPath",
69
+ "filePath",
70
+ ]);
71
+ }