@skalfa/skalfa-app-core 1.0.3 → 1.0.5
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/commands/blueprint.d.ts +3 -0
- package/dist/commands/blueprint.js +306 -0
- package/dist/commands/cli.d.ts +6 -0
- package/dist/commands/cli.js +107 -0
- package/dist/commands/use-pdf.d.ts +1 -0
- package/dist/commands/use-pdf.js +17 -0
- package/dist/hooks.d.ts +14 -0
- package/dist/hooks.js +63 -0
- package/dist/index.d.ts +2 -14
- package/dist/index.js +2 -63
- package/dist/validation/index.d.ts +1 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation/use-validation.d.ts +2 -0
- package/dist/validation/use-validation.js +23 -0
- package/dist/validation/validation.d.ts +0 -1
- package/dist/validation/validation.js +0 -22
- package/package.json +2 -1
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { conversion } from "../conversion/conversion";
|
|
4
|
+
import { logger } from "../logger";
|
|
5
|
+
const renderJS = (value, indent = 0) => {
|
|
6
|
+
const pad = " ".repeat(indent);
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
if (!value.length)
|
|
9
|
+
return "[]";
|
|
10
|
+
if (value.every(v => typeof v === "string")) {
|
|
11
|
+
return `[${value.map(v => JSON.stringify(v)).join(", ")}]`;
|
|
12
|
+
}
|
|
13
|
+
return `[\n${value
|
|
14
|
+
.map(v => pad + " " + renderJS(v, indent + 2))
|
|
15
|
+
.join(",\n")}\n${pad}]`;
|
|
16
|
+
}
|
|
17
|
+
if (value && typeof value === "object") {
|
|
18
|
+
const entries = Object.entries(value);
|
|
19
|
+
if (!entries.length)
|
|
20
|
+
return "{}";
|
|
21
|
+
return `{\n${entries
|
|
22
|
+
.map(([k, v]) => `${pad} ${k}: ${renderJS(v, indent + 2)}`)
|
|
23
|
+
.join(",\n")}\n${pad}}`;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value === "string")
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
return String(value);
|
|
28
|
+
};
|
|
29
|
+
function extractValidationArray(def = "") {
|
|
30
|
+
const rules = [];
|
|
31
|
+
if (def.includes("required"))
|
|
32
|
+
rules.push("required");
|
|
33
|
+
if (def.includes("email"))
|
|
34
|
+
rules.push("email");
|
|
35
|
+
if (def.includes("url"))
|
|
36
|
+
rules.push("url");
|
|
37
|
+
const min = def.match(/min,(\d+)/);
|
|
38
|
+
if (min)
|
|
39
|
+
rules.push(`min:${min[1]}`);
|
|
40
|
+
const max = def.match(/max,(\d+)/);
|
|
41
|
+
if (max)
|
|
42
|
+
rules.push(`max:${max[1]}`);
|
|
43
|
+
return rules;
|
|
44
|
+
}
|
|
45
|
+
function extractFormType(rules) {
|
|
46
|
+
const rule = rules.find(r => r.startsWith("form:"));
|
|
47
|
+
return rule ? rule.replace("form:", "") : undefined;
|
|
48
|
+
}
|
|
49
|
+
function inferFormType(pageDef = "", modelDef = "") {
|
|
50
|
+
const explicit = pageDef.split("|");
|
|
51
|
+
if (explicit && explicit[0] && explicit[0] != "text")
|
|
52
|
+
return explicit[0];
|
|
53
|
+
if (modelDef.includes("type:integer") || modelDef.includes("type:float")) {
|
|
54
|
+
return "number";
|
|
55
|
+
}
|
|
56
|
+
if (modelDef.includes("type:date"))
|
|
57
|
+
return "date";
|
|
58
|
+
if (modelDef.includes("type:datetime"))
|
|
59
|
+
return "datetime";
|
|
60
|
+
if (modelDef.includes("type:image"))
|
|
61
|
+
return "image";
|
|
62
|
+
return "default";
|
|
63
|
+
}
|
|
64
|
+
function parseFeatures(features) {
|
|
65
|
+
const controlBar = [];
|
|
66
|
+
const action = [];
|
|
67
|
+
if (!features) {
|
|
68
|
+
return { controlBar: ["CREATE"], action: ["EDIT", "DELETE"] };
|
|
69
|
+
}
|
|
70
|
+
const list = features.split(" ");
|
|
71
|
+
if (list.includes("create"))
|
|
72
|
+
controlBar.push("CREATE");
|
|
73
|
+
controlBar.push("SEARCH", "SORT", "SELECTABLE");
|
|
74
|
+
if (list.includes("import"))
|
|
75
|
+
controlBar.push("IMPORT");
|
|
76
|
+
if (list.includes("export"))
|
|
77
|
+
controlBar.push("EXPORT");
|
|
78
|
+
if (list.includes("print"))
|
|
79
|
+
controlBar.push("PRINT");
|
|
80
|
+
if (list.includes("update") || list.includes("edit"))
|
|
81
|
+
action.push("EDIT");
|
|
82
|
+
if (list.includes("delete"))
|
|
83
|
+
action.push("DELETE");
|
|
84
|
+
if (list.includes("detail"))
|
|
85
|
+
action.push("DETAIL");
|
|
86
|
+
return { controlBar, action };
|
|
87
|
+
}
|
|
88
|
+
function parseModelSchema(schema = {}) {
|
|
89
|
+
const columns = [];
|
|
90
|
+
const forms = [];
|
|
91
|
+
const details = [];
|
|
92
|
+
for (const [field, def] of Object.entries(schema)) {
|
|
93
|
+
const label = conversion.strPascal(field, " ");
|
|
94
|
+
if (def.includes("selectable")) {
|
|
95
|
+
columns.push({ selector: field, label });
|
|
96
|
+
details.push({ label, item: field });
|
|
97
|
+
}
|
|
98
|
+
if (def.includes("fillable")) {
|
|
99
|
+
const fieldType = inferFormType("", def);
|
|
100
|
+
forms.push({
|
|
101
|
+
...(fieldType != "default" ? { type: fieldType } : {}),
|
|
102
|
+
construction: {
|
|
103
|
+
name: field,
|
|
104
|
+
label,
|
|
105
|
+
placeholder: "Masukkan " + label.toLowerCase(),
|
|
106
|
+
validations: extractValidationArray(def)
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { columns, forms, details };
|
|
112
|
+
}
|
|
113
|
+
function resolvePath(page, controllers, model) {
|
|
114
|
+
if (page.path)
|
|
115
|
+
return page.path;
|
|
116
|
+
if (Array.isArray(controllers)) {
|
|
117
|
+
const match = controllers;
|
|
118
|
+
if (match)
|
|
119
|
+
return "/" + match[1];
|
|
120
|
+
}
|
|
121
|
+
return "/" + model.split("/").pop();
|
|
122
|
+
}
|
|
123
|
+
function parsePageSchema(pageSchema, modelSchema = {}) {
|
|
124
|
+
const columns = [];
|
|
125
|
+
const forms = [];
|
|
126
|
+
const details = [];
|
|
127
|
+
for (const [field, def] of Object.entries(pageSchema)) {
|
|
128
|
+
const rules = def.replace(/\|/g, " ").split(" ").filter(Boolean);
|
|
129
|
+
const defaultLabel = conversion.strPascal(field, " ");
|
|
130
|
+
const colLabelRule = rules.find(r => r.includes("column:label,"));
|
|
131
|
+
const colLabel = conversion.strPascal(colLabelRule ? colLabelRule.split(",")[1] : defaultLabel, " ");
|
|
132
|
+
const formLabelRule = rules.find(r => r.includes("form:label,"));
|
|
133
|
+
const formLabel = conversion.strPascal(formLabelRule ? formLabelRule.split(",")[1] : (colLabelRule ? colLabel : defaultLabel), " ");
|
|
134
|
+
const detailLabel = colLabelRule ? colLabel : (formLabelRule ? formLabel : defaultLabel);
|
|
135
|
+
const hasColumn = rules.some(r => r.includes("column:"));
|
|
136
|
+
const hasForm = rules.some(r => r.includes("form:")) || rules.every(r => !r.includes("column:"));
|
|
137
|
+
const hasDetail = rules.includes("detail");
|
|
138
|
+
if (hasColumn) {
|
|
139
|
+
columns.push({
|
|
140
|
+
selector: field,
|
|
141
|
+
label: colLabel,
|
|
142
|
+
...(rules.includes("column:sortable") || rules.includes("sortable") ? { sortable: true } : {})
|
|
143
|
+
});
|
|
144
|
+
if (!hasDetail)
|
|
145
|
+
details.push({ label: detailLabel, item: field });
|
|
146
|
+
}
|
|
147
|
+
if (hasForm) {
|
|
148
|
+
const typeRules = rules.filter(r => !r.startsWith("form:label,"));
|
|
149
|
+
const typeRule = extractFormType(typeRules);
|
|
150
|
+
let fieldType = inferFormType(typeRule, modelSchema[field] || "");
|
|
151
|
+
let selectPath = "";
|
|
152
|
+
const selectRule = rules.find(r => r.startsWith("select,") || r.startsWith("form:select,"));
|
|
153
|
+
if (selectRule) {
|
|
154
|
+
fieldType = "select";
|
|
155
|
+
selectPath = selectRule.split(",")[1];
|
|
156
|
+
}
|
|
157
|
+
if (typeRule === "check")
|
|
158
|
+
fieldType = "boolean";
|
|
159
|
+
if (typeRule === "currency")
|
|
160
|
+
fieldType = "currency";
|
|
161
|
+
if (typeRule === "image")
|
|
162
|
+
fieldType = "image";
|
|
163
|
+
if (typeRule === "date")
|
|
164
|
+
fieldType = "date";
|
|
165
|
+
if (typeRule === "time")
|
|
166
|
+
fieldType = "time";
|
|
167
|
+
let col;
|
|
168
|
+
const colRule = rules.find(r => /col,(\d+)/.test(r));
|
|
169
|
+
if (colRule) {
|
|
170
|
+
const n = Number(colRule.split(',').pop());
|
|
171
|
+
if (n >= 1 && n <= 12)
|
|
172
|
+
col = n;
|
|
173
|
+
}
|
|
174
|
+
forms.push({
|
|
175
|
+
...(fieldType != "default" && fieldType != "text" ? { type: fieldType } : {}),
|
|
176
|
+
...(col ? { col } : {}),
|
|
177
|
+
construction: {
|
|
178
|
+
name: field,
|
|
179
|
+
label: formLabel,
|
|
180
|
+
placeholder: "Masukkan " + formLabel.toLowerCase(),
|
|
181
|
+
validations: extractValidationArray(modelSchema[field] || ""),
|
|
182
|
+
...(selectPath ? { serverOptionControl: { path: selectPath } } : {}),
|
|
183
|
+
...(rules.includes("wrap") ? { wrap: true } : {})
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (hasDetail)
|
|
188
|
+
details.push({ label: detailLabel, item: field });
|
|
189
|
+
}
|
|
190
|
+
const formMap = new Map();
|
|
191
|
+
forms.forEach(f => formMap.set(f.construction.name, f));
|
|
192
|
+
const nestedForms = [];
|
|
193
|
+
forms.forEach(f => {
|
|
194
|
+
const name = f.construction.name;
|
|
195
|
+
if (name.includes(".")) {
|
|
196
|
+
const parts = name.split(".");
|
|
197
|
+
const selfName = parts.pop();
|
|
198
|
+
const parentName = parts.join(".");
|
|
199
|
+
const parent = formMap.get(parentName);
|
|
200
|
+
if (parent) {
|
|
201
|
+
if (!parent.type)
|
|
202
|
+
parent.type = "cluster";
|
|
203
|
+
if (!parent.construction.fields)
|
|
204
|
+
parent.construction.fields = [];
|
|
205
|
+
f.construction.name = selfName;
|
|
206
|
+
parent.construction.fields.push(f);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
nestedForms.push(f);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
nestedForms.push(f);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
return { columns, forms: nestedForms, details };
|
|
217
|
+
}
|
|
218
|
+
function loadBlueprintFiles(dir = "blueprints") {
|
|
219
|
+
const basePath = path.join(process.cwd(), dir);
|
|
220
|
+
if (!fs.existsSync(basePath)) {
|
|
221
|
+
throw new Error("Blueprint folder not found");
|
|
222
|
+
}
|
|
223
|
+
return fs.readdirSync(basePath)
|
|
224
|
+
.filter(f => f.endsWith(".blueprint.json"))
|
|
225
|
+
.map(file => {
|
|
226
|
+
const fullPath = path.join(basePath, file);
|
|
227
|
+
const content = JSON.parse(fs.readFileSync(fullPath, "utf-8"));
|
|
228
|
+
if (!Array.isArray(content)) {
|
|
229
|
+
throw new Error(`${file} must export array of blueprints`);
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
file: file.replace(".blueprint.json", ""),
|
|
233
|
+
blueprints: content,
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const blueprintMarker = `// ============================================
|
|
238
|
+
// ## file THIS FILE IS AUTO-GENERATED BY BLUEPRINT
|
|
239
|
+
// ?? Blueprint : {{ blueprint }}
|
|
240
|
+
// !! If this comment is removed, blueprint engine WILL NOT override this file.
|
|
241
|
+
// ============================================
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
`;
|
|
245
|
+
// ===============================
|
|
246
|
+
// ## Main generator
|
|
247
|
+
// ===============================
|
|
248
|
+
export async function blueprint(options) {
|
|
249
|
+
const stub = fs.readFileSync(path.join(process.cwd(), "/utils/commands/stubs/table-blueprint.stub"), "utf-8");
|
|
250
|
+
const loaded = loadBlueprintFiles();
|
|
251
|
+
for (const file of loaded) {
|
|
252
|
+
for (const bp of file.blueprints) {
|
|
253
|
+
const pagesToGenerate = { ...(bp.pages || {}) };
|
|
254
|
+
for (const [key, val] of Object.entries(bp)) {
|
|
255
|
+
if (typeof val === "object" && val["schema"]) {
|
|
256
|
+
pagesToGenerate[key] = val;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const [key, page] of Object.entries(pagesToGenerate)) {
|
|
260
|
+
const route = key;
|
|
261
|
+
const name = conversion.strPascal(route.split("/").pop());
|
|
262
|
+
if (options?.only && !options.only.includes(name))
|
|
263
|
+
continue;
|
|
264
|
+
const outDir = path.join(process.cwd(), "app", route);
|
|
265
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
266
|
+
const filePath = path.join(outDir, "page.tsx");
|
|
267
|
+
if (fs.existsSync(filePath)) {
|
|
268
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
269
|
+
if (!content.includes("AUTO-GENERATED BY BLUEPRINT")) {
|
|
270
|
+
logger.info(`Skip overridden file: ${filePath}`);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const schema = bp.schema ?? {};
|
|
275
|
+
const parsed = page?.schema ? parsePageSchema(page.schema, schema) : parseModelSchema(schema);
|
|
276
|
+
const { controlBar, action } = parseFeatures(page?.features);
|
|
277
|
+
const fetchPath = resolvePath(page, bp.controllers, bp.model);
|
|
278
|
+
let properties = `
|
|
279
|
+
fetchControl={{
|
|
280
|
+
path: "${fetchPath}"
|
|
281
|
+
}}
|
|
282
|
+
columnControl={${renderJS(parsed.columns, 8)}}
|
|
283
|
+
formControl={{
|
|
284
|
+
fields: ${renderJS(parsed.forms, 10)}
|
|
285
|
+
}}
|
|
286
|
+
`;
|
|
287
|
+
if (parsed.details) {
|
|
288
|
+
properties += ` detailControl={${renderJS(parsed.details, 8)}}\n`;
|
|
289
|
+
}
|
|
290
|
+
if (controlBar.length) {
|
|
291
|
+
properties += ` controlBar={${JSON.stringify(controlBar)}}\n`;
|
|
292
|
+
}
|
|
293
|
+
if (action.length) {
|
|
294
|
+
properties += ` actionControl={${JSON.stringify(action)}}\n`;
|
|
295
|
+
}
|
|
296
|
+
const content = stub
|
|
297
|
+
.replace(/{{ marker }}/g, blueprintMarker.replace(/{{ blueprint }}/g, file.file + ".blueprint.json"))
|
|
298
|
+
.replace(/{{ name }}/g, name)
|
|
299
|
+
.replace(/{{ title }}/g, name)
|
|
300
|
+
.replace(/{{ properties }}/g, properties);
|
|
301
|
+
fs.writeFileSync(path.join(outDir, "page.tsx"), content);
|
|
302
|
+
logger.info(`Generated: ${filePath}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
export declare const usePdfCommand: Command;
|
|
3
|
+
export declare const blueprintCommand: Command;
|
|
4
|
+
export declare const barrelsCommand: Command;
|
|
5
|
+
export declare const watchBarrelsCommand: Command;
|
|
6
|
+
export declare function runCli(): void;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { usePdf } from "./use-pdf";
|
|
3
|
+
import { blueprint } from "./blueprint";
|
|
4
|
+
import { logger } from "../logger";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { exec, execSync } from "child_process";
|
|
8
|
+
// =====================================>
|
|
9
|
+
// ## Command: use-pdf
|
|
10
|
+
// =====================================>
|
|
11
|
+
export const usePdfCommand = new Command("use-pdf")
|
|
12
|
+
.description("Copy pdf.worker.min.mjs ke folder public/")
|
|
13
|
+
.action(() => {
|
|
14
|
+
usePdf();
|
|
15
|
+
});
|
|
16
|
+
// =====================================>
|
|
17
|
+
// ## Command: blueprint
|
|
18
|
+
// =====================================>
|
|
19
|
+
export const blueprintCommand = new Command("blueprint")
|
|
20
|
+
.description("Generate blueprint")
|
|
21
|
+
.option("-o, --only <names...>", "Run only specific blueprints")
|
|
22
|
+
.action(async (opts) => {
|
|
23
|
+
await blueprint({ only: opts.only });
|
|
24
|
+
logger.info("Success run all blueprints!");
|
|
25
|
+
process.exit(0);
|
|
26
|
+
});
|
|
27
|
+
// =====================================>
|
|
28
|
+
// ## Command: barrels (run once)
|
|
29
|
+
// =====================================>
|
|
30
|
+
export const barrelsCommand = new Command("barrels")
|
|
31
|
+
.description("Generate barrels auto-imports using barrelsby")
|
|
32
|
+
.action(() => {
|
|
33
|
+
const rootDir = process.cwd();
|
|
34
|
+
const configPath = path.join(rootDir, "barrels.json");
|
|
35
|
+
if (!fs.existsSync(configPath)) {
|
|
36
|
+
logger.error("barrels.json config file not found at project root");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
logger.info("Generating barrels...");
|
|
40
|
+
try {
|
|
41
|
+
execSync("npx barrelsby -c barrels.json", { cwd: rootDir, stdio: "inherit" });
|
|
42
|
+
logger.info("Barrels successfully generated!");
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
logger.error(`Failed to generate barrels: ${err}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
// =====================================>
|
|
51
|
+
// ## Command: watch:barrels (file watcher)
|
|
52
|
+
// =====================================>
|
|
53
|
+
export const watchBarrelsCommand = new Command("watch:barrels")
|
|
54
|
+
.description("Watch directories and update barrels automatically on file changes")
|
|
55
|
+
.action(async () => {
|
|
56
|
+
const rootDir = process.cwd();
|
|
57
|
+
const configPath = path.join(rootDir, "barrels.json");
|
|
58
|
+
if (!fs.existsSync(configPath)) {
|
|
59
|
+
logger.error("barrels.json config file not found at project root");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
let config = {};
|
|
63
|
+
try {
|
|
64
|
+
const configText = fs.readFileSync(configPath, "utf8");
|
|
65
|
+
config = JSON.parse(configText);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
logger.error(`Failed to parse barrels.json: ${err}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const directories = Array.isArray(config.directory) ? config.directory : [config.directory];
|
|
72
|
+
// Run barrels once at startup
|
|
73
|
+
logger.info("Initializing barrels generation...");
|
|
74
|
+
try {
|
|
75
|
+
execSync("npx barrelsby -c barrels.json", { cwd: rootDir });
|
|
76
|
+
}
|
|
77
|
+
catch { }
|
|
78
|
+
directories.forEach((dir) => {
|
|
79
|
+
const absoluteDir = path.join(rootDir, dir);
|
|
80
|
+
if (!fs.existsSync(absoluteDir)) {
|
|
81
|
+
logger.error(`Barrels error: ${absoluteDir} directory not found`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
fs.watch(absoluteDir, { recursive: true }, (_, filename) => {
|
|
85
|
+
if (filename && (filename.endsWith(".ts") || filename.endsWith(".tsx")) && filename !== "index.ts") {
|
|
86
|
+
exec("npx barrelsby -c barrels.json", { cwd: rootDir }, (error) => {
|
|
87
|
+
if (error) {
|
|
88
|
+
logger.error(`Failed to update barrels for ${dir}: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
logger.info(`Barrels updated: ${path.join(dir, "index.ts")}`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
logger.start("Barrels watcher running for: " + directories.join(", "));
|
|
98
|
+
});
|
|
99
|
+
export function runCli() {
|
|
100
|
+
const program = new Command();
|
|
101
|
+
program.name("skalfa").description("Skalfa-app CLI").version("1.0.0");
|
|
102
|
+
program.addCommand(usePdfCommand);
|
|
103
|
+
program.addCommand(blueprintCommand);
|
|
104
|
+
program.addCommand(barrelsCommand);
|
|
105
|
+
program.addCommand(watchBarrelsCommand);
|
|
106
|
+
program.parse(process.argv);
|
|
107
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function usePdf(): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { logger } from "../logger";
|
|
5
|
+
export function usePdf() {
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const projectRoot = path.join(__dirname, "..", "..");
|
|
9
|
+
const source = path.join(projectRoot, "node_modules", "pdfjs-dist", "legacy", "build", "pdf.worker.min.mjs");
|
|
10
|
+
const target = path.join(projectRoot, "public", "pdf.worker.min.mjs");
|
|
11
|
+
if (!fs.existsSync(source)) {
|
|
12
|
+
logger.error(`Gagal: pdf.worker.min.mjs tidak ditemukan.`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
fs.copyFileSync(source, target);
|
|
16
|
+
logger.info("Berhasil memindahkan worker ke public/pdf.worker.min.mjs");
|
|
17
|
+
}
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const useResponsive: () => {
|
|
2
|
+
isXs: boolean;
|
|
3
|
+
isSm: boolean;
|
|
4
|
+
isMd: boolean;
|
|
5
|
+
isLg: boolean;
|
|
6
|
+
isXl: boolean;
|
|
7
|
+
isMobile: boolean;
|
|
8
|
+
isTablet: boolean;
|
|
9
|
+
isDesktop: boolean;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function useKeyboardOpen(): boolean;
|
|
14
|
+
export declare const useLazySearch: (keyword: string) => string[];
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
// ==============================>
|
|
4
|
+
// ## Detect device size
|
|
5
|
+
// ==============================>
|
|
6
|
+
export const useResponsive = () => {
|
|
7
|
+
const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (typeof window === "undefined")
|
|
10
|
+
return;
|
|
11
|
+
const handleResize = () => {
|
|
12
|
+
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
|
|
13
|
+
};
|
|
14
|
+
handleResize();
|
|
15
|
+
window.addEventListener("resize", handleResize);
|
|
16
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
17
|
+
}, []);
|
|
18
|
+
return {
|
|
19
|
+
isXs: windowSize.width < 640,
|
|
20
|
+
isSm: windowSize.width < 768,
|
|
21
|
+
isMd: windowSize.width < 1024,
|
|
22
|
+
isLg: windowSize.width < 1280,
|
|
23
|
+
isXl: windowSize.width >= 1280,
|
|
24
|
+
isMobile: windowSize.width < 768,
|
|
25
|
+
isTablet: windowSize.width >= 768 && windowSize.width < 1024,
|
|
26
|
+
isDesktop: windowSize.width >= 1024,
|
|
27
|
+
width: windowSize.width,
|
|
28
|
+
height: windowSize.height,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
// ==============================>
|
|
32
|
+
// ## Detect keyboard open
|
|
33
|
+
// ==============================>
|
|
34
|
+
export function useKeyboardOpen() {
|
|
35
|
+
const [isKeyboardOpen, setIsKeyboardOpen] = useState(false);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const handleResize = () => {
|
|
38
|
+
if (window.visualViewport) {
|
|
39
|
+
const viewportHeight = window.visualViewport.height;
|
|
40
|
+
const windowHeight = window.innerHeight;
|
|
41
|
+
setIsKeyboardOpen(viewportHeight < windowHeight);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
window.visualViewport?.addEventListener("resize", handleResize);
|
|
45
|
+
return () => window.visualViewport?.removeEventListener("resize", handleResize);
|
|
46
|
+
}, []);
|
|
47
|
+
return isKeyboardOpen;
|
|
48
|
+
}
|
|
49
|
+
// ==============================>
|
|
50
|
+
// ## Search with typing reference
|
|
51
|
+
// ==============================>
|
|
52
|
+
export const useLazySearch = (keyword) => {
|
|
53
|
+
const [keywordSearch, setKeywordSearch] = useState("");
|
|
54
|
+
const [doSearch, setDoSearch] = useState(false);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (keyword != undefined) {
|
|
57
|
+
const delaySearch = setTimeout(() => setDoSearch(!doSearch), 500);
|
|
58
|
+
return () => clearTimeout(delaySearch);
|
|
59
|
+
}
|
|
60
|
+
}, [keyword]);
|
|
61
|
+
useEffect(() => setKeywordSearch(keyword), [doSearch]);
|
|
62
|
+
return [keywordSearch];
|
|
63
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -11,17 +11,5 @@ export * from "./conversion";
|
|
|
11
11
|
export * from "./shortcut";
|
|
12
12
|
export * from "./logger";
|
|
13
13
|
export * from "./registry";
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
isSm: boolean;
|
|
17
|
-
isMd: boolean;
|
|
18
|
-
isLg: boolean;
|
|
19
|
-
isXl: boolean;
|
|
20
|
-
isMobile: boolean;
|
|
21
|
-
isTablet: boolean;
|
|
22
|
-
isDesktop: boolean;
|
|
23
|
-
width: number;
|
|
24
|
-
height: number;
|
|
25
|
-
};
|
|
26
|
-
export declare function useKeyboardOpen(): boolean;
|
|
27
|
-
export declare const useLazySearch: (keyword: string) => string[];
|
|
14
|
+
export * from "./commands/cli";
|
|
15
|
+
export * from "./hooks";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
3
1
|
// ==============================>
|
|
4
2
|
// ## Export all from core utils
|
|
5
3
|
// ==============================>
|
|
@@ -16,64 +14,5 @@ export * from "./conversion";
|
|
|
16
14
|
export * from "./shortcut";
|
|
17
15
|
export * from "./logger";
|
|
18
16
|
export * from "./registry";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// ==============================>
|
|
22
|
-
export const useResponsive = () => {
|
|
23
|
-
const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
if (typeof window === "undefined")
|
|
26
|
-
return;
|
|
27
|
-
const handleResize = () => {
|
|
28
|
-
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
|
|
29
|
-
};
|
|
30
|
-
handleResize();
|
|
31
|
-
window.addEventListener("resize", handleResize);
|
|
32
|
-
return () => window.removeEventListener("resize", handleResize);
|
|
33
|
-
}, []);
|
|
34
|
-
return {
|
|
35
|
-
isXs: windowSize.width < 640,
|
|
36
|
-
isSm: windowSize.width < 768,
|
|
37
|
-
isMd: windowSize.width < 1024,
|
|
38
|
-
isLg: windowSize.width < 1280,
|
|
39
|
-
isXl: windowSize.width >= 1280,
|
|
40
|
-
isMobile: windowSize.width < 768,
|
|
41
|
-
isTablet: windowSize.width >= 768 && windowSize.width < 1024,
|
|
42
|
-
isDesktop: windowSize.width >= 1024,
|
|
43
|
-
width: windowSize.width,
|
|
44
|
-
height: windowSize.height,
|
|
45
|
-
};
|
|
46
|
-
};
|
|
47
|
-
// ==============================>
|
|
48
|
-
// ## Detect keyboard open
|
|
49
|
-
// ==============================>
|
|
50
|
-
export function useKeyboardOpen() {
|
|
51
|
-
const [isKeyboardOpen, setIsKeyboardOpen] = useState(false);
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
const handleResize = () => {
|
|
54
|
-
if (window.visualViewport) {
|
|
55
|
-
const viewportHeight = window.visualViewport.height;
|
|
56
|
-
const windowHeight = window.innerHeight;
|
|
57
|
-
setIsKeyboardOpen(viewportHeight < windowHeight);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
window.visualViewport?.addEventListener("resize", handleResize);
|
|
61
|
-
return () => window.visualViewport?.removeEventListener("resize", handleResize);
|
|
62
|
-
}, []);
|
|
63
|
-
return isKeyboardOpen;
|
|
64
|
-
}
|
|
65
|
-
// ==============================>
|
|
66
|
-
// ## Search with typing reference
|
|
67
|
-
// ==============================>
|
|
68
|
-
export const useLazySearch = (keyword) => {
|
|
69
|
-
const [keywordSearch, setKeywordSearch] = useState("");
|
|
70
|
-
const [doSearch, setDoSearch] = useState(false);
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
if (keyword != undefined) {
|
|
73
|
-
const delaySearch = setTimeout(() => setDoSearch(!doSearch), 500);
|
|
74
|
-
return () => clearTimeout(delaySearch);
|
|
75
|
-
}
|
|
76
|
-
}, [keyword]);
|
|
77
|
-
useEffect(() => setKeywordSearch(keyword), [doSearch]);
|
|
78
|
-
return [keywordSearch];
|
|
79
|
-
};
|
|
17
|
+
export * from "./commands/cli";
|
|
18
|
+
export * from "./hooks";
|
package/dist/validation/index.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { validation } from "./validation";
|
|
4
|
+
// =========================>
|
|
5
|
+
// ## Check validation Hook
|
|
6
|
+
// =========================>
|
|
7
|
+
export const useValidation = (value = "", rules = "", includes = "", sleep = false) => {
|
|
8
|
+
const [message, setMessage] = useState("");
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (rules && !sleep) {
|
|
11
|
+
const { valid, message } = validation.check({ value, rules });
|
|
12
|
+
setMessage(valid ? "" : message);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
setMessage("");
|
|
16
|
+
}
|
|
17
|
+
}, [value, rules, sleep]);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (includes)
|
|
20
|
+
setMessage(includes);
|
|
21
|
+
}, [includes]);
|
|
22
|
+
return [message, setMessage];
|
|
23
|
+
};
|
|
@@ -14,5 +14,4 @@ export declare const validation: {
|
|
|
14
14
|
hasRules: (rules?: Rule[] | string, ruleName?: string | string[]) => boolean;
|
|
15
15
|
getRules: (rules: Rule[] | string, ruleName: string) => string | undefined;
|
|
16
16
|
};
|
|
17
|
-
export declare const useValidation: (value?: any, rules?: Rule[] | string, includes?: string, sleep?: boolean) => [string, (message: string) => void];
|
|
18
17
|
export {};
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
3
1
|
import validator from "validator";
|
|
4
2
|
import { validationLangs } from "./validation.langs";
|
|
5
3
|
// ==========================>
|
|
@@ -128,23 +126,3 @@ export const validation = {
|
|
|
128
126
|
return found ? found.split(":")[1] : undefined;
|
|
129
127
|
}
|
|
130
128
|
};
|
|
131
|
-
// =========================>
|
|
132
|
-
// ## Check validation Hook
|
|
133
|
-
// =========================>
|
|
134
|
-
export const useValidation = (value = "", rules = "", includes = "", sleep = false) => {
|
|
135
|
-
const [message, setMessage] = useState("");
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
if (rules && !sleep) {
|
|
138
|
-
const { valid, message } = validation.check({ value, rules });
|
|
139
|
-
setMessage(valid ? "" : message);
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
setMessage("");
|
|
143
|
-
}
|
|
144
|
-
}, [value, rules, sleep]);
|
|
145
|
-
useEffect(() => {
|
|
146
|
-
if (includes)
|
|
147
|
-
setMessage(includes);
|
|
148
|
-
}, [includes]);
|
|
149
|
-
return [message, setMessage];
|
|
150
|
-
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skalfa/skalfa-app-core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Core framework engine and foundational utility hook library for the Skalfa Next.js frontend.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"axios": "^1.12.0",
|
|
22
22
|
"clsx": "^2.1.1",
|
|
23
|
+
"commander": "^12.1.0",
|
|
23
24
|
"crypto-js": "^4.2.0",
|
|
24
25
|
"js-cookie": "^3.0.5",
|
|
25
26
|
"lz-string": "^1.5.0",
|