@skalfa/skalfa-app 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.
- package/.env.example +44 -0
- package/README.md +28 -0
- package/app/auth/edit/page.tsx +65 -0
- package/app/auth/login/page.tsx +63 -0
- package/app/auth/me/page.tsx +58 -0
- package/app/auth/register/page.tsx +69 -0
- package/app/auth/verify/page.tsx +53 -0
- package/app/dashboard/layout.tsx +47 -0
- package/app/dashboard/page.tsx +9 -0
- package/app/dashboard/user/page.tsx +77 -0
- package/app/index.ts +14 -0
- package/app/layout.tsx +38 -0
- package/app/page.tsx +13 -0
- package/barrels.json +6 -0
- package/blueprints/starter.blueprint.json +103 -0
- package/components/base.components/accordion/Accordion.component.tsx +82 -0
- package/components/base.components/breadcrumb/Breadcrumb.component.tsx +80 -0
- package/components/base.components/button/Button.component.tsx +91 -0
- package/components/base.components/button/IconButton.component.tsx +88 -0
- package/components/base.components/button/button.decorate.ts +82 -0
- package/components/base.components/card/AlertCard.component.tsx +69 -0
- package/components/base.components/card/Card.component.tsx +25 -0
- package/components/base.components/card/DashboardCard.component.tsx +44 -0
- package/components/base.components/card/GalleryCard.component.tsx +50 -0
- package/components/base.components/card/ProductCard.component.tsx +65 -0
- package/components/base.components/card/ProfileCard.component.tsx +71 -0
- package/components/base.components/carousel/Carousel.component.tsx +113 -0
- package/components/base.components/chip/Chip.component.tsx +39 -0
- package/components/base.components/document/DocumentViewer.component.tsx +164 -0
- package/components/base.components/document/ExportExcel.component.tsx +340 -0
- package/components/base.components/document/ImportExcel.component.tsx +315 -0
- package/components/base.components/document/PrintTable.component.tsx +204 -0
- package/components/base.components/document/RenderPDF.component.tsx +416 -0
- package/components/base.components/index.ts +85 -0
- package/components/base.components/input/Checkbox.component.tsx +109 -0
- package/components/base.components/input/Input.component.tsx +332 -0
- package/components/base.components/input/InputCheckbox.component.tsx +174 -0
- package/components/base.components/input/InputCurrency.component.tsx +163 -0
- package/components/base.components/input/InputDate.component.tsx +352 -0
- package/components/base.components/input/InputDatetime.component.tsx +260 -0
- package/components/base.components/input/InputDocument.component.tsx +352 -0
- package/components/base.components/input/InputImage.component.tsx +533 -0
- package/components/base.components/input/InputMap.component.tsx +318 -0
- package/components/base.components/input/InputNumber.component.tsx +192 -0
- package/components/base.components/input/InputOtp.component.tsx +169 -0
- package/components/base.components/input/InputPassword.component.tsx +236 -0
- package/components/base.components/input/InputRadio.component.tsx +175 -0
- package/components/base.components/input/InputTime.component.tsx +276 -0
- package/components/base.components/input/InputValues.component.tsx +68 -0
- package/components/base.components/input/Radio.component.tsx +102 -0
- package/components/base.components/input/Select.component.tsx +541 -0
- package/components/base.components/modal/BottomSheet.component.tsx +246 -0
- package/components/base.components/modal/FloatingPage.component.tsx +104 -0
- package/components/base.components/modal/Modal.component.tsx +96 -0
- package/components/base.components/modal/ModalConfirm.component.tsx +218 -0
- package/components/base.components/modal/Toast.component.tsx +126 -0
- package/components/base.components/nav/Bottombar.component.tsx +116 -0
- package/components/base.components/nav/Footer.component.tsx +144 -0
- package/components/base.components/nav/Headbar.component.tsx +104 -0
- package/components/base.components/nav/Navbar.component.tsx +100 -0
- package/components/base.components/nav/Sidebar.component.tsx +301 -0
- package/components/base.components/nav/Tabbar.component.tsx +60 -0
- package/components/base.components/nav/Wizard.component.tsx +73 -0
- package/components/base.components/supervision/FormSupervision.component.tsx +434 -0
- package/components/base.components/supervision/TableSupervision.component.tsx +697 -0
- package/components/base.components/table/ControlBar.component.tsx +497 -0
- package/components/base.components/table/FilterComponent.tsx +518 -0
- package/components/base.components/table/Pagination.component.tsx +159 -0
- package/components/base.components/table/Table.component.tsx +469 -0
- package/components/base.components/typography/TypographyArticle.component.tsx +26 -0
- package/components/base.components/typography/TypographyColumn.component.tsx +20 -0
- package/components/base.components/typography/TypographyContent.component.tsx +20 -0
- package/components/base.components/typography/TypographyTips.component.tsx +20 -0
- package/components/base.components/wrap/Draggable.component.tsx +303 -0
- package/components/base.components/wrap/IDBProvider.tsx +12 -0
- package/components/base.components/wrap/Image.component.tsx +10 -0
- package/components/base.components/wrap/OutsideClick.component.tsx +48 -0
- package/components/base.components/wrap/ScrollContainer.component.tsx +104 -0
- package/components/base.components/wrap/ShortcutProvider.tsx +57 -0
- package/components/base.components/wrap/Swipe.component.tsx +93 -0
- package/components/construct.components/example.tsx +1 -0
- package/components/construct.components/index.ts +5 -0
- package/components/index.ts +3 -0
- package/components/structure.components/example.tsx +1 -0
- package/components/structure.components/index.ts +5 -0
- package/contexts/AppProvider.tsx +12 -0
- package/contexts/Auth.context.tsx +64 -0
- package/contexts/Toggle.context.tsx +44 -0
- package/contexts/index.ts +7 -0
- package/eslint.config.mjs +34 -0
- package/langs/index.ts +1 -0
- package/langs/validation.langs.ts +17 -0
- package/next.config.ts +17 -0
- package/package.json +43 -0
- package/postcss.config.mjs +12 -0
- package/public/204.svg +19 -0
- package/public/500.svg +39 -0
- package/public/images/avatar.jpg +0 -0
- package/public/images/example.png +0 -0
- package/schema/idb/app.schema.ts +9 -0
- package/schema/index.ts +5 -0
- package/styles/globals.css +231 -0
- package/styles/tailwind.safelist +69 -0
- package/tailwind.config.ts +10 -0
- package/tsconfig.json +35 -0
- package/utils/commands/barrels.ts +28 -0
- package/utils/commands/blueprint.ts +421 -0
- package/utils/commands/light.ts +21 -0
- package/utils/commands/logger.ts +42 -0
- package/utils/commands/stubs/table-blueprint.stub +13 -0
- package/utils/commands/use-pdf.ts +29 -0
- package/utils/index.ts +3 -0
package/tsconfig.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include" : ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
|
|
3
|
+
"exclude" : ["node_modules", ".next"],
|
|
4
|
+
"compilerOptions" : {
|
|
5
|
+
"target" : "ES2017",
|
|
6
|
+
"lib" : ["dom", "dom.iterable", "esnext"],
|
|
7
|
+
"allowJs" : true,
|
|
8
|
+
"skipLibCheck" : true,
|
|
9
|
+
"strict" : true,
|
|
10
|
+
"noEmit" : true,
|
|
11
|
+
"esModuleInterop" : true,
|
|
12
|
+
"module" : "esnext",
|
|
13
|
+
"moduleResolution" : "bundler",
|
|
14
|
+
"resolveJsonModule" : true,
|
|
15
|
+
"isolatedModules" : true,
|
|
16
|
+
"jsx" : "react-jsx",
|
|
17
|
+
"incremental" : true,
|
|
18
|
+
"paths" : {
|
|
19
|
+
"@/*" : ["./*"],
|
|
20
|
+
"@utils" : ["./utils/index.ts"],
|
|
21
|
+
"@utils/*" : ["./utils/*"],
|
|
22
|
+
"@components" : ["./components/index.ts"],
|
|
23
|
+
"@components/*" : ["./components/*", "./components/base.components/*", "./components/construct.components/*", "./components/structure.components/*"],
|
|
24
|
+
"@contexts" : ["./contexts/index.ts"],
|
|
25
|
+
"@contexts/*" : ["./contexts/*"],
|
|
26
|
+
"@app" : ["./app/index.ts"],
|
|
27
|
+
"@app/*" : ["./app/*"],
|
|
28
|
+
"@schema" : ["./schema/index.ts"],
|
|
29
|
+
"@schema/*" : ["./schema/*"],
|
|
30
|
+
"@styles/*" : ["./styles/*"],
|
|
31
|
+
"@public/*" : ["./public/*"]
|
|
32
|
+
},
|
|
33
|
+
"plugins" : [{ "name": "next" }]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { exec } from "child_process";
|
|
4
|
+
import { logger } from "./logger";
|
|
5
|
+
|
|
6
|
+
const rootDir = path.resolve();
|
|
7
|
+
const configText = fs.readFileSync("barrels.json", "utf8");
|
|
8
|
+
const config = JSON.parse(configText);
|
|
9
|
+
const directories: string[] = Array.isArray(config.directory) ? config.directory : [config.directory];
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
directories.forEach((dir) => {
|
|
13
|
+
const absoluteDir = path.join(rootDir, dir);
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(absoluteDir)) {
|
|
16
|
+
logger.error(`Barrels error: ${absoluteDir} directory not found`)
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fs.watch(absoluteDir, { recursive: true }, (_, filename) => {
|
|
21
|
+
if (filename && (filename.endsWith(".ts") || filename.endsWith(".tsx")) && filename !== "index.ts") {
|
|
22
|
+
exec("npx barrelsby -c barrels.json", { cwd: rootDir })
|
|
23
|
+
logger.info("Barrels updated " + absoluteDir + "/index.ts")
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
logger.start("Barrels watched " + directories.join(", "))
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { ValidationRules, conversion } from "@skalfa/skalfa-app-core";
|
|
4
|
+
import { logger } from "./logger";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
type SchemaMap = Record<string, string>
|
|
9
|
+
|
|
10
|
+
type ColumnItem = {
|
|
11
|
+
selector ?: string
|
|
12
|
+
label : string
|
|
13
|
+
sortable ?: boolean
|
|
14
|
+
item ?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type FormItem = {
|
|
18
|
+
type ?: string
|
|
19
|
+
col ?: number
|
|
20
|
+
construction : {
|
|
21
|
+
name : string
|
|
22
|
+
label : string
|
|
23
|
+
placeholder : string
|
|
24
|
+
validations : ValidationRules[]
|
|
25
|
+
serverOptionControl ?: {
|
|
26
|
+
path : string
|
|
27
|
+
}
|
|
28
|
+
fields ?: FormItem[]
|
|
29
|
+
wrap ?: boolean
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type DetailItem = {
|
|
34
|
+
label : string
|
|
35
|
+
item : string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type ParsedSchema = {
|
|
39
|
+
columns : ColumnItem[]
|
|
40
|
+
forms : FormItem[]
|
|
41
|
+
details : DetailItem[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type BlueprintPage = {
|
|
45
|
+
features ?: string
|
|
46
|
+
path ?: string
|
|
47
|
+
schema ?: Record<string, string>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type BlueprintStruct = {
|
|
51
|
+
model : string
|
|
52
|
+
controllers ?: string[]
|
|
53
|
+
schema ?: SchemaMap
|
|
54
|
+
pages ?: false | Record<string, BlueprintPage>
|
|
55
|
+
[key: string]: any
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type LoadedBlueprintFile = {
|
|
59
|
+
file : string
|
|
60
|
+
blueprints : BlueprintStruct[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
const renderJS = (value: unknown, indent = 0): string => {
|
|
66
|
+
const pad = " ".repeat(indent)
|
|
67
|
+
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
if (!value.length) return "[]"
|
|
70
|
+
|
|
71
|
+
if (value.every(v => typeof v === "string")) {
|
|
72
|
+
return `[${value.map(v => JSON.stringify(v)).join(", ")}]`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return `[\n${value
|
|
76
|
+
.map(v => pad + " " + renderJS(v, indent + 2))
|
|
77
|
+
.join(",\n")}\n${pad}]`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (value && typeof value === "object") {
|
|
81
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
82
|
+
if (!entries.length) return "{}"
|
|
83
|
+
|
|
84
|
+
return `{\n${entries
|
|
85
|
+
.map(([k, v]) => `${pad} ${k}: ${renderJS(v, indent + 2)}`)
|
|
86
|
+
.join(",\n")}\n${pad}}`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof value === "string") return JSON.stringify(value)
|
|
90
|
+
|
|
91
|
+
return String(value)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function extractValidationArray(def: string = ""): ValidationRules[] {
|
|
95
|
+
const rules: ValidationRules[] = []
|
|
96
|
+
|
|
97
|
+
if (def.includes("required")) rules.push("required")
|
|
98
|
+
|
|
99
|
+
if (def.includes("email")) rules.push("email")
|
|
100
|
+
|
|
101
|
+
if (def.includes("url")) rules.push("url")
|
|
102
|
+
|
|
103
|
+
const min = def.match(/min,(\d+)/)
|
|
104
|
+
if (min) rules.push(`min:${min[1]}`)
|
|
105
|
+
|
|
106
|
+
const max = def.match(/max,(\d+)/)
|
|
107
|
+
if (max) rules.push(`max:${max[1]}`)
|
|
108
|
+
|
|
109
|
+
return rules
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractFormType(rules: string[]): string | undefined {
|
|
113
|
+
const rule = rules.find(r => r.startsWith("form:"))
|
|
114
|
+
return rule ? rule.replace("form:", "") : undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function inferFormType(pageDef = "", modelDef = ""): string {
|
|
118
|
+
const explicit = pageDef.split("|")
|
|
119
|
+
if (explicit && explicit[0] && explicit[0] != "text") return explicit[0]
|
|
120
|
+
|
|
121
|
+
if (modelDef.includes("type:integer") || modelDef.includes("type:float")) {
|
|
122
|
+
return "number"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (modelDef.includes("type:date")) return "date"
|
|
126
|
+
if (modelDef.includes("type:datetime")) return "datetime"
|
|
127
|
+
if (modelDef.includes("type:image")) return "image"
|
|
128
|
+
|
|
129
|
+
return "default"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseFeatures(features?: string): { controlBar: string[]; action: string[] } {
|
|
133
|
+
const controlBar: string[] = []
|
|
134
|
+
const action: string[] = []
|
|
135
|
+
|
|
136
|
+
if (!features) {
|
|
137
|
+
return { controlBar: ["CREATE"], action: ["EDIT", "DELETE"] }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const list = features.split(" ")
|
|
141
|
+
|
|
142
|
+
if (list.includes("create")) controlBar.push("CREATE")
|
|
143
|
+
|
|
144
|
+
controlBar.push("SEARCH", "SORT", "SELECTABLE")
|
|
145
|
+
|
|
146
|
+
if (list.includes("import")) controlBar.push("IMPORT")
|
|
147
|
+
if (list.includes("export")) controlBar.push("EXPORT")
|
|
148
|
+
if (list.includes("print")) controlBar.push("PRINT")
|
|
149
|
+
|
|
150
|
+
if (list.includes("update") || list.includes("edit")) action.push("EDIT")
|
|
151
|
+
if (list.includes("delete")) action.push("DELETE")
|
|
152
|
+
if (list.includes("detail")) action.push("DETAIL")
|
|
153
|
+
|
|
154
|
+
return { controlBar, action }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseModelSchema(schema: SchemaMap = {}): ParsedSchema {
|
|
158
|
+
const columns: ColumnItem[] = []
|
|
159
|
+
const forms: FormItem[] = []
|
|
160
|
+
const details: DetailItem[] = []
|
|
161
|
+
|
|
162
|
+
for (const [field, def] of Object.entries(schema)) {
|
|
163
|
+
const label = conversion.strPascal(field, " ")
|
|
164
|
+
|
|
165
|
+
if (def.includes("selectable")) {
|
|
166
|
+
columns.push({ selector: field, label })
|
|
167
|
+
details.push({ label, item: field })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (def.includes("fillable")) {
|
|
171
|
+
const fieldType = inferFormType("", def);
|
|
172
|
+
forms.push({
|
|
173
|
+
...(fieldType != "default" ? { type: fieldType } : {}),
|
|
174
|
+
construction: {
|
|
175
|
+
name: field,
|
|
176
|
+
label,
|
|
177
|
+
placeholder: "Masukkan " + label.toLowerCase(),
|
|
178
|
+
validations: extractValidationArray(def)
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { columns, forms, details }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resolvePath(page: BlueprintPage, controllers: string[] | boolean | undefined, model: string): string {
|
|
188
|
+
if (page.path) return page.path
|
|
189
|
+
|
|
190
|
+
if (Array.isArray(controllers)) {
|
|
191
|
+
const match = controllers
|
|
192
|
+
if (match) return "/" + match[1]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return "/" + model.split("/").pop()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parsePageSchema(pageSchema: Record<string, string>, modelSchema: SchemaMap = {}): ParsedSchema {
|
|
199
|
+
const columns: ColumnItem[] = []
|
|
200
|
+
const forms: FormItem[] = []
|
|
201
|
+
const details: DetailItem[] = []
|
|
202
|
+
|
|
203
|
+
for (const [field, def] of Object.entries(pageSchema)) {
|
|
204
|
+
const rules = def.replace(/\|/g, " ").split(" ").filter(Boolean)
|
|
205
|
+
|
|
206
|
+
const defaultLabel = conversion.strPascal(field, " ")
|
|
207
|
+
|
|
208
|
+
const colLabelRule = rules.find(r => r.includes("column:label,"))
|
|
209
|
+
const colLabel = conversion.strPascal(colLabelRule ? colLabelRule.split(",")[1] : defaultLabel, " ")
|
|
210
|
+
|
|
211
|
+
const formLabelRule = rules.find(r => r.includes("form:label,"))
|
|
212
|
+
const formLabel = conversion.strPascal(formLabelRule ? formLabelRule.split(",")[1] : (colLabelRule ? colLabel : defaultLabel), " ")
|
|
213
|
+
|
|
214
|
+
const detailLabel = colLabelRule ? colLabel : (formLabelRule ? formLabel : defaultLabel)
|
|
215
|
+
|
|
216
|
+
const hasColumn = rules.some(r => r.includes("column:"))
|
|
217
|
+
const hasForm = rules.some(r => r.includes("form:")) || rules.every(r => !r.includes("column:"))
|
|
218
|
+
|
|
219
|
+
const hasDetail = rules.includes("detail")
|
|
220
|
+
|
|
221
|
+
if (hasColumn) {
|
|
222
|
+
columns.push({
|
|
223
|
+
selector: field,
|
|
224
|
+
label: colLabel,
|
|
225
|
+
...(rules.includes("column:sortable") || rules.includes("sortable") ? { sortable: true } : {})
|
|
226
|
+
})
|
|
227
|
+
if (!hasDetail) details.push({ label: detailLabel, item: field })
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (hasForm) {
|
|
231
|
+
const typeRules = rules.filter(r => !r.startsWith("form:label,"))
|
|
232
|
+
const typeRule = extractFormType(typeRules)
|
|
233
|
+
let fieldType = inferFormType(typeRule, modelSchema[field] || "");
|
|
234
|
+
|
|
235
|
+
let selectPath = ""
|
|
236
|
+
const selectRule = rules.find(r => r.startsWith("select,") || r.startsWith("form:select,"))
|
|
237
|
+
if (selectRule) {
|
|
238
|
+
fieldType = "select"
|
|
239
|
+
selectPath = selectRule.split(",")[1]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (typeRule === "check") fieldType = "boolean"
|
|
243
|
+
if (typeRule === "currency") fieldType = "currency"
|
|
244
|
+
if (typeRule === "image") fieldType = "image"
|
|
245
|
+
if (typeRule === "date") fieldType = "date"
|
|
246
|
+
if (typeRule === "time") fieldType = "time"
|
|
247
|
+
|
|
248
|
+
let col: number | undefined
|
|
249
|
+
const colRule = rules.find(r => /col,(\d+)/.test(r))
|
|
250
|
+
|
|
251
|
+
if (colRule) {
|
|
252
|
+
const n = Number(colRule.split(',').pop())
|
|
253
|
+
if (n >= 1 && n <= 12) col = n
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
forms.push({
|
|
257
|
+
...(fieldType != "default" && fieldType != "text" ? { type: fieldType } : {}),
|
|
258
|
+
...(col ? { col } : {}),
|
|
259
|
+
construction: {
|
|
260
|
+
name: field,
|
|
261
|
+
label: formLabel,
|
|
262
|
+
placeholder: "Masukkan " + formLabel.toLowerCase(),
|
|
263
|
+
validations: extractValidationArray(modelSchema[field] || ""),
|
|
264
|
+
...(selectPath ? { serverOptionControl: { path: selectPath } } : {}),
|
|
265
|
+
...(rules.includes("wrap") ? { wrap: true } : {})
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (hasDetail) details.push({ label: detailLabel, item: field })
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const formMap = new Map<string, FormItem>()
|
|
274
|
+
forms.forEach(f => formMap.set(f.construction.name, f))
|
|
275
|
+
|
|
276
|
+
const nestedForms: FormItem[] = []
|
|
277
|
+
|
|
278
|
+
forms.forEach(f => {
|
|
279
|
+
const name = f.construction.name
|
|
280
|
+
if (name.includes(".")) {
|
|
281
|
+
const parts = name.split(".")
|
|
282
|
+
const selfName = parts.pop() as string
|
|
283
|
+
const parentName = parts.join(".")
|
|
284
|
+
const parent = formMap.get(parentName)
|
|
285
|
+
|
|
286
|
+
if (parent) {
|
|
287
|
+
if (!parent.type) parent.type = "cluster"
|
|
288
|
+
if (!parent.construction.fields) parent.construction.fields = []
|
|
289
|
+
|
|
290
|
+
f.construction.name = selfName
|
|
291
|
+
|
|
292
|
+
parent.construction.fields.push(f)
|
|
293
|
+
} else {
|
|
294
|
+
nestedForms.push(f)
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
nestedForms.push(f)
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
return { columns, forms: nestedForms, details }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
function loadBlueprintFiles(dir: string = "blueprints"): LoadedBlueprintFile[] {
|
|
307
|
+
const basePath = path.join(process.cwd(), dir)
|
|
308
|
+
|
|
309
|
+
if (!fs.existsSync(basePath)) {
|
|
310
|
+
throw new Error("Blueprint folder not found")
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return fs.readdirSync(basePath)
|
|
314
|
+
.filter(f => f.endsWith(".blueprint.json"))
|
|
315
|
+
.map(file => {
|
|
316
|
+
const fullPath = path.join(basePath, file)
|
|
317
|
+
const content = JSON.parse(fs.readFileSync(fullPath, "utf-8"))
|
|
318
|
+
|
|
319
|
+
if (!Array.isArray(content)) {
|
|
320
|
+
throw new Error(`${file} must export array of blueprints`)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
file: file.replace(".blueprint.json", ""),
|
|
325
|
+
blueprints: content as BlueprintStruct[],
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const blueprintMarker = `// ============================================
|
|
331
|
+
// ## file THIS FILE IS AUTO-GENERATED BY BLUEPRINT
|
|
332
|
+
// ?? Blueprint : {{ blueprint }}
|
|
333
|
+
// !! If this comment is removed, blueprint engine WILL NOT override this file.
|
|
334
|
+
// ============================================
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
`
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
// ===============================
|
|
341
|
+
// ## Main generator
|
|
342
|
+
// ===============================
|
|
343
|
+
|
|
344
|
+
export async function blueprint(options?: { only?: string[] }): Promise<void> {
|
|
345
|
+
const stub = fs.readFileSync(path.join(process.cwd(), "/utils/commands/stubs/table-blueprint.stub"), "utf-8")
|
|
346
|
+
|
|
347
|
+
const loaded = loadBlueprintFiles()
|
|
348
|
+
|
|
349
|
+
for (const file of loaded) {
|
|
350
|
+
for (const bp of file.blueprints) {
|
|
351
|
+
|
|
352
|
+
const pagesToGenerate: Record<string, BlueprintPage> = { ...(bp.pages || {}) }
|
|
353
|
+
|
|
354
|
+
for (const [key, val] of Object.entries(bp)) {
|
|
355
|
+
if (typeof val === "object" && val["schema"]) {
|
|
356
|
+
pagesToGenerate[key] = val as BlueprintPage
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const [key, page] of Object.entries(pagesToGenerate)) {
|
|
361
|
+
|
|
362
|
+
const route = key;
|
|
363
|
+
const name = conversion.strPascal(route.split("/").pop() as string)
|
|
364
|
+
|
|
365
|
+
if (options?.only && !options.only.includes(name)) continue
|
|
366
|
+
|
|
367
|
+
const outDir = path.join(process.cwd(), "app", route)
|
|
368
|
+
fs.mkdirSync(outDir, { recursive: true })
|
|
369
|
+
|
|
370
|
+
const filePath = path.join(outDir, "page.tsx");
|
|
371
|
+
|
|
372
|
+
if (fs.existsSync(filePath)) {
|
|
373
|
+
const content = fs.readFileSync(filePath, "utf-8")
|
|
374
|
+
|
|
375
|
+
if (!content.includes("AUTO-GENERATED BY BLUEPRINT")) {
|
|
376
|
+
logger.info(`Skip overridden file: ${filePath}`)
|
|
377
|
+
continue
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const schema = bp.schema ?? {}
|
|
382
|
+
|
|
383
|
+
const parsed: ParsedSchema = page?.schema ? parsePageSchema(page.schema, schema) : parseModelSchema(schema)
|
|
384
|
+
|
|
385
|
+
const { controlBar, action } = parseFeatures(page?.features)
|
|
386
|
+
|
|
387
|
+
const fetchPath = resolvePath(page, bp.controllers, bp.model)
|
|
388
|
+
|
|
389
|
+
let properties = `
|
|
390
|
+
fetchControl={{
|
|
391
|
+
path: "${fetchPath}"
|
|
392
|
+
}}
|
|
393
|
+
columnControl={${renderJS(parsed.columns, 8)}}
|
|
394
|
+
formControl={{
|
|
395
|
+
fields: ${renderJS(parsed.forms, 10)}
|
|
396
|
+
}}
|
|
397
|
+
`
|
|
398
|
+
if (parsed.details) {
|
|
399
|
+
properties += ` detailControl={${renderJS(parsed.details, 8)}}\n`
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (controlBar.length) {
|
|
403
|
+
properties += ` controlBar={${JSON.stringify(controlBar)}}\n`
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (action.length) {
|
|
407
|
+
properties += ` actionControl={${JSON.stringify(action)}}\n`
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const content = stub
|
|
411
|
+
.replace(/{{ marker }}/g, blueprintMarker.replace(/{{ blueprint }}/g, file.file + ".blueprint.json"))
|
|
412
|
+
.replace(/{{ name }}/g, name)
|
|
413
|
+
.replace(/{{ title }}/g, name)
|
|
414
|
+
.replace(/{{ properties }}/g, properties)
|
|
415
|
+
|
|
416
|
+
fs.writeFileSync(path.join(outDir, "page.tsx"), content)
|
|
417
|
+
logger.info(`Generated: ${filePath}`)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { usePdf } from "./use-pdf";
|
|
3
|
+
import { blueprint } from "./blueprint";
|
|
4
|
+
import { logger } from "./logger";
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program.name("light").description("Next Light CLI").version("1.0.0");
|
|
9
|
+
|
|
10
|
+
program.command("use-pdf").description("Copy pdf.worker.min.mjs ke folder public/").action(usePdf );
|
|
11
|
+
program.command("blueprint")
|
|
12
|
+
.option("-o, --only <names...>", "Run only specific blueprints")
|
|
13
|
+
.description("Generate blueprint")
|
|
14
|
+
.action(async (opts) => {
|
|
15
|
+
await blueprint({ only: opts.only })
|
|
16
|
+
|
|
17
|
+
logger.info("Success run all blueprints!")
|
|
18
|
+
process.exit(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
program.parse();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
type LogType = "start" | "info" | "error" | "warning" | "cavity" | "socket" | "cavityError" | "socketError";
|
|
3
|
+
|
|
4
|
+
const colors: Record<LogType | "default", string> = {
|
|
5
|
+
default : "\x1b[0m", // default
|
|
6
|
+
start : "\x1b[32m", // green
|
|
7
|
+
info : "\x1b[36m", // cyan
|
|
8
|
+
error : "\x1b[31m", // red
|
|
9
|
+
warning : "\x1b[33m", // yellow
|
|
10
|
+
cavity : "\x1b[34m", // blue
|
|
11
|
+
cavityError : "\x1b[31m", // red
|
|
12
|
+
socket : "\x1b[35m", // magenta
|
|
13
|
+
socketError : "\x1b[31m", // red
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const prefixes: Record<LogType, string> = {
|
|
17
|
+
start : "START",
|
|
18
|
+
info : "INFO",
|
|
19
|
+
error : "ERROR",
|
|
20
|
+
warning : "WARNING",
|
|
21
|
+
cavity : "CAVITY",
|
|
22
|
+
socket : "SOCKET",
|
|
23
|
+
cavityError : "CAVITY ERROR",
|
|
24
|
+
socketError : "SOCKET ERROR",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function log(type: LogType, ...msg: unknown[]) {
|
|
28
|
+
const color = colors[type];
|
|
29
|
+
const prefix = prefixes[type];
|
|
30
|
+
console.log(`${color}[${prefix}]${colors.default}`, ...msg);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const logger = {
|
|
34
|
+
start : (...msg: unknown[]) => log("start", ...msg),
|
|
35
|
+
info : (...msg: unknown[]) => log("info", ...msg),
|
|
36
|
+
error : (...msg: unknown[]) => log("error", ...msg),
|
|
37
|
+
warning : (...msg: unknown[]) => log("warning", ...msg),
|
|
38
|
+
cavity : (...msg: unknown[]) => log("cavity", ...msg),
|
|
39
|
+
cavityError : (...msg: unknown[]) => log("cavityError", ...msg),
|
|
40
|
+
socket : (...msg: unknown[]) => log("socket", ...msg),
|
|
41
|
+
socketError : (...msg: unknown[]) => log("socketError", ...msg),
|
|
42
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { logger } from "./logger";
|
|
5
|
+
|
|
6
|
+
export function usePdf() {
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const projectRoot = path.join(__dirname, "..", "..");
|
|
10
|
+
|
|
11
|
+
const source = path.join(
|
|
12
|
+
projectRoot,
|
|
13
|
+
"node_modules",
|
|
14
|
+
"pdfjs-dist",
|
|
15
|
+
"legacy",
|
|
16
|
+
"build",
|
|
17
|
+
"pdf.worker.min.mjs"
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const target = path.join(projectRoot, "public", "pdf.worker.min.mjs");
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(source)) {
|
|
23
|
+
logger.error(`Gagal: pdf.worker.min.mjs tidak ditemukan.`)
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fs.copyFileSync(source, target);
|
|
28
|
+
logger.info("Berhasil memindahkan worker ke public/pdf.worker.min.mjs")
|
|
29
|
+
}
|
package/utils/index.ts
ADDED