milkio 0.0.10 → 0.0.12

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.
Files changed (45) hide show
  1. package/.co.toml +0 -0
  2. package/api-test/index.ts +64 -0
  3. package/c.ts +39 -57
  4. package/defines/define-api-test.ts +3 -3
  5. package/defines/define-api.ts +3 -3
  6. package/defines/define-http-handler.ts +60 -69
  7. package/defines/define-middleware.ts +2 -2
  8. package/defines/define-use.ts +6 -6
  9. package/index.ts +23 -22
  10. package/kernel/context.ts +1 -2
  11. package/kernel/fail.ts +6 -6
  12. package/kernel/logger.ts +38 -38
  13. package/kernel/meta.ts +5 -5
  14. package/kernel/middleware.ts +16 -16
  15. package/kernel/milkio.ts +70 -95
  16. package/kernel/runtime.ts +2 -7
  17. package/kernel/validate.ts +5 -5
  18. package/package.json +4 -1
  19. package/scripts/gen-insignificant.ts +261 -0
  20. package/scripts/gen-significant.ts +176 -0
  21. package/{scripts → scripts-del}/build-cookbook.ts +119 -119
  22. package/scripts-del/build-dto.ts +65 -0
  23. package/{scripts → scripts-del}/generate/generate-app-partial.ts +31 -31
  24. package/{scripts → scripts-del}/generate/generate-app.ts +41 -41
  25. package/scripts-del/generate/generate-database.ts +22 -0
  26. package/scripts-del/generate-partial.ts +15 -0
  27. package/scripts-del/generate.ts +23 -0
  28. package/templates/api.ts +4 -4
  29. package/types.ts +29 -19
  30. package/utils/create-template.ts +5 -5
  31. package/utils/create-ulid.ts +3 -3
  32. package/utils/env-to-boolean.ts +5 -5
  33. package/utils/env-to-number.ts +2 -2
  34. package/utils/env-to-string.ts +2 -2
  35. package/utils/exec.ts +12 -12
  36. package/utils/handle-catch-error.ts +10 -10
  37. package/utils/remove-dir.ts +11 -11
  38. package/utils/tson.ts +2 -2
  39. package/defines/define-api-test-handler.ts +0 -71
  40. package/kernel/config.ts +0 -14
  41. package/scripts/build-dto.ts +0 -65
  42. package/scripts/generate/generate-database.ts +0 -22
  43. package/scripts/generate-database.ts +0 -23
  44. package/scripts/generate-partial.ts +0 -15
  45. package/scripts/generate.ts +0 -23
@@ -0,0 +1,261 @@
1
+ /* eslint-disable no-console, @typescript-eslint/no-dynamic-delete */
2
+
3
+ import { $ } from "bun"
4
+ import { join } from "node:path"
5
+ import { cwd } from "node:process"
6
+ import { existsSync } from "node:fs"
7
+ import { writeFile, readFile, mkdir, copyFile } from "node:fs/promises"
8
+ import { MilkioConfig, TSON, type Cookbook } from ".."
9
+
10
+ export default async () => {
11
+ const schema = await import("../../../generated/api-schema")
12
+ const paths = Object.keys(schema.default.apiMethodsSchema)
13
+
14
+ console.log('')
15
+ console.time(`🧊 Cookbook Stage`)
16
+
17
+ const cookbook: Cookbook = {}
18
+ for (const path of paths) {
19
+ // const module = await import(/* @vite-ignore */ join(`../../../src/apps/${path}`));
20
+ const code = (await readFile(join(cwd(), `./src/apps/${path}.ts`))).toString()
21
+ const codeLines = code.split("\n")
22
+ let title
23
+ let desc
24
+ const descRaw = /\n\/\*\*\n[\s\S]+?\*\//.exec(code)?.[0] ?? ""
25
+
26
+ if (descRaw) {
27
+ const descRawLines = descRaw.split("\n")
28
+ if (descRawLines.at(0)?.trim() === "") descRawLines.shift()
29
+ if (descRawLines.at(-1)?.trim() === "") descRawLines.pop()
30
+ let first = true
31
+ for (let index = 0; index < descRawLines.length; index++) {
32
+ const descRawLine = descRawLines[index].replace(/^[/ ]+?[*]*/, "").replace(/[*]*\/$/, "")
33
+
34
+ if (!descRawLine) continue
35
+ if (first) {
36
+ title = descRawLine.replace(/#/g, "").trim()
37
+ // Originally the title was in the first line, desc is the rest of it, now desc contains complete markdown content.
38
+ // continue;
39
+ }
40
+ first = false
41
+ desc = (desc ?? "") + "\n" + descRawLine.trim()
42
+ }
43
+ }
44
+
45
+ let apiParams = /action\([\s\S]+?\)/.exec(code)?.[0] ?? "" // The intention of the following code is to extract the parameter part of the action.
46
+ apiParams = /\([\s\S]*,/.exec(apiParams)?.[0] ?? ""
47
+ apiParams = apiParams.slice(0, -1)
48
+ apiParams = apiParams.slice(/[\s\S]+?:/.exec(apiParams)?.[0].length)
49
+ const apiParamsLines = apiParams.split("\n") // The intention of the following code is to remove extra spaces, which will make the code look more beautiful.
50
+ if (apiParamsLines.at(-1)?.trim() === "") apiParamsLines.pop()
51
+ if (apiParamsLines.at(-1)?.trim() === "") apiParamsLines.pop()
52
+ let spaceNumber = 0
53
+ for (const char of apiParamsLines.at(-1) ?? "") {
54
+ if (char === " ") spaceNumber++
55
+ else break
56
+ }
57
+ for (let index = 0; index < apiParamsLines.length; index++) {
58
+ const line = apiParamsLines[index]
59
+ let spaceNumberForThisLine = 0
60
+ for (const char of line) {
61
+ if (char === " ") spaceNumberForThisLine++
62
+ else break
63
+ }
64
+ if (spaceNumberForThisLine >= spaceNumber) {
65
+ apiParamsLines[index] = line.slice(spaceNumber)
66
+ } else {
67
+ apiParamsLines[index] = line.slice(spaceNumberForThisLine)
68
+ }
69
+ }
70
+ apiParams = apiParamsLines.join("\n")
71
+
72
+ // Find the code for the API testing section.
73
+ const apiTestsCodeChars = []
74
+ let apiTestsStartIndex = undefined as undefined | number
75
+ let semicolonMatch = 0
76
+ let semicolonMax = 0
77
+ for (let index = 0; index < codeLines.length; index++) {
78
+ const codeLine = codeLines[index]
79
+ if (apiTestsStartIndex === undefined && !codeLine.includes("defineApiTest(")) continue
80
+ if (apiTestsStartIndex === undefined) apiTestsStartIndex = index
81
+ const codeChars = codeLine.split("")
82
+ for (const codeChar of codeChars) {
83
+ if (codeChar === "[") {
84
+ semicolonMatch++
85
+ semicolonMax++
86
+ }
87
+ if (semicolonMatch !== 0) apiTestsCodeChars.push(codeChar)
88
+ if (codeChar === "]") semicolonMatch--
89
+ }
90
+ if (semicolonMatch === 0 && semicolonMax >= 1) {
91
+ break
92
+ }
93
+ apiTestsCodeChars.push("\n")
94
+ }
95
+
96
+ // Find the code for each API test case.
97
+ const apiCaseCodes: Array<string> = []
98
+ let currentApiCaseCode = undefined as undefined | Array<string>
99
+ let apiTestCaseStartIndex = undefined as undefined | number
100
+ let apiTestCaseMatch = 0
101
+ for (let index = 0; index < apiTestsCodeChars.length; index++) {
102
+ const apiTestsCodeChar = apiTestsCodeChars[index]
103
+ if (apiTestCaseStartIndex === undefined && apiTestsCodeChar === "{") {
104
+ currentApiCaseCode = []
105
+ apiTestCaseStartIndex = index
106
+ }
107
+ if (apiTestsCodeChar === "{") {
108
+ apiTestCaseMatch++
109
+ }
110
+
111
+ if (apiTestCaseMatch !== 0) currentApiCaseCode!.push(apiTestsCodeChar)
112
+
113
+ if (apiTestsCodeChar === "}") {
114
+ apiTestCaseMatch--
115
+ if (apiTestCaseMatch === 0) {
116
+ apiCaseCodes.push(currentApiCaseCode!.join(""))
117
+ currentApiCaseCode = undefined
118
+ apiTestCaseStartIndex = undefined
119
+ }
120
+ }
121
+ }
122
+
123
+ const apiCases: Array<{
124
+ name: string;
125
+ handler: string;
126
+ }> = []
127
+
128
+ for (let index = 0; index < apiCaseCodes.length; index++) {
129
+ const code = apiCaseCodes[index]
130
+ const name = /name:[\s\S]+?,/.exec(code)?.[0]?.slice(5, -1)?.trim().slice(1, -1) ?? ""
131
+ const handlerChars = /handler:[\s\S]*/.exec(code)?.[0]?.split("") ?? []
132
+ let handler = "" // Find the main code of the handler.
133
+ let handlerStartIndex = undefined as undefined | number
134
+ let handlerMatch = 0
135
+ for (let index = 0; index < handlerChars.length; index++) {
136
+ const handlerChar = handlerChars[index]
137
+ if (handlerStartIndex !== undefined && handlerChar === "{") handlerStartIndex = index
138
+ if (handlerChar === "{") handlerMatch++
139
+ if (handlerMatch !== 0) handler = handler + handlerChar
140
+ if (handlerChar === "}") handlerMatch--
141
+ if (handlerStartIndex !== undefined && handlerMatch === 0) break
142
+ }
143
+ handler = handler.slice(1, -1)
144
+
145
+ const handlerLines = handler.split("\n") // The intention of the following code is to remove extra spaces, which will make the code look more beautiful.
146
+ if (handlerLines.at(-1)?.trim() === "") handlerLines.pop()
147
+ if (handlerLines.at(-1)?.trim() === "") handlerLines.pop()
148
+ if (handlerLines.at(0)?.trim() === "") handlerLines.shift()
149
+ if (handlerLines.at(0)?.trim() === "") handlerLines.shift()
150
+ let spaceNumber = 0
151
+ for (const char of handlerLines.at(-1) ?? "") {
152
+ if (char === " ") spaceNumber++
153
+ else break
154
+ }
155
+ for (let index = 0; index < handlerLines.length; index++) {
156
+ const line = handlerLines[index]
157
+ let spaceNumberForThisLine = 0
158
+ for (const char of line) {
159
+ if (char === " ") spaceNumberForThisLine++
160
+ else break
161
+ }
162
+ if (spaceNumberForThisLine >= spaceNumber) {
163
+ handlerLines[index] = line.slice(spaceNumber)
164
+ } else {
165
+ handlerLines[index] = line.slice(spaceNumberForThisLine)
166
+ }
167
+ }
168
+ handler = handlerLines.join("\n")
169
+
170
+ apiCases.push({
171
+ name,
172
+ handler
173
+ })
174
+ }
175
+
176
+ // This value has been deprecated because TypeScript types can already replace it well
177
+ // let paramsSchema;
178
+ // try {
179
+ // const moduleGenerated = await import(/* @vite-ignore */ `../../../generated/products/apps/${path}`);
180
+ // paramsSchema = moduleGenerated.paramsSchema.schemas[0]?.properties?.data;
181
+ // } catch (error) {}
182
+
183
+ cookbook[path] = {
184
+ title,
185
+ desc,
186
+ params: apiParams,
187
+ cases: apiCases
188
+ }
189
+ }
190
+
191
+ /**
192
+ * -- indexes
193
+ */
194
+
195
+ const indexes: Record<string, Array<string>> = {}
196
+ const folderIndexes: Record<string, Array<string>> = {}
197
+ indexes["(root)"] = []
198
+ folderIndexes["(root)"] = []
199
+ for (const path in cookbook) {
200
+ if (!path.includes("/")) indexes["(root)"].push(path)
201
+ }
202
+ for (const path in cookbook) {
203
+ const dirnames = path.split("/")
204
+ for (let index = 0; index < dirnames.length - 1; index++) {
205
+ const dirpath = dirnames.slice(0, index + 1).join("/")
206
+ if (!indexes[dirpath]) indexes[dirpath] = []
207
+ if (!folderIndexes[dirpath]) folderIndexes[dirpath] = []
208
+ if (index + 1 === dirnames.length - 1) {
209
+ indexes[dirpath].push(path)
210
+ } else {
211
+ const childDirpath = dirnames.slice(0, index + 2).join("/")
212
+ if (folderIndexes[dirpath].includes(childDirpath)) continue
213
+ folderIndexes[dirpath].push(childDirpath)
214
+ }
215
+ }
216
+ }
217
+ for (const path in folderIndexes) {
218
+ if (path.includes("/") || path === "(root)") continue
219
+ folderIndexes["(root)"].push(path)
220
+ }
221
+
222
+ const readme = (await readFile(join(cwd(), "src", "apps", "README.md"))).toString()
223
+ Object.keys(indexes).forEach((key) => indexes[key].length === 0 && delete indexes[key])
224
+ const generatedAt = new Date()
225
+
226
+ await writeFile(
227
+ join(cwd(), `./generated/cookbook.json`),
228
+ TSON.stringify({
229
+ cookbook,
230
+ readme,
231
+ indexes,
232
+ folderIndexes,
233
+ generatedAt
234
+ })
235
+ )
236
+
237
+ console.timeEnd(`🧊 Cookbook Stage`)
238
+ console.log(``)
239
+
240
+ console.time(`🧊 DTO Stage`)
241
+ await $`bun run ./node_modules/typescript/bin/tsc --outDir "./packages/dto/project"`.quiet()
242
+ await Bun.build({
243
+ entrypoints: ["./packages/dto/index.ts"],
244
+ outdir: "./packages/dto/dist",
245
+ target: 'browser',
246
+ minify: true
247
+ })
248
+ console.timeEnd(`🧊 DTO Stage`)
249
+ console.log(``)
250
+
251
+ if (!existsSync(join(cwd(), "milkio.toml"))) return
252
+ const milkioConfig = (await import(join(cwd(), "milkio.toml"))).default as MilkioConfig
253
+ if (!milkioConfig?.generate?.significant) return
254
+ let i = 0
255
+ for (const command of milkioConfig.generate.significant) {
256
+ ++i
257
+ console.time(`🧊 Insignificant Stage (LINE ${i})`)
258
+ await $`${{ raw: command }}`
259
+ console.timeEnd(`🧊 Insignificant Stage (LINE ${i})`)
260
+ }
261
+ }
@@ -0,0 +1,176 @@
1
+ /* eslint-disable no-console */
2
+
3
+ import ejs from "ejs"
4
+ import { join } from "node:path"
5
+ import { existsSync, mkdirSync } from "node:fs"
6
+ import { cwd, exit } from "node:process"
7
+ import { unlink, writeFile } from "node:fs/promises"
8
+ import { camel, hyphen } from "@poech/camel-hump-under"
9
+ import { $, Glob } from "bun"
10
+ import { MilkioConfig } from ".."
11
+
12
+ export default async () => {
13
+ // Delete the files generated in the past and regenerate them
14
+ try {
15
+ await unlink(join(cwd(), "generated", "api-schema.ts"))
16
+ } catch (error) { } // Maybe the file does not exist
17
+
18
+ // Make sure that the existing directories are all present
19
+ existsSync(join("generated")) || mkdirSync(join("generated"))
20
+ existsSync(join("generated", "raw")) || mkdirSync(join("generated", "raw"))
21
+ existsSync(join("generated", "raw", "apps")) || mkdirSync(join("generated", "raw", "apps"))
22
+
23
+ if (!existsSync(join("generated", "README.md"))) {
24
+ await writeFile(join("generated", "README.md"), "⚠️ All files in this directory are generated by milkio. Please do not modify the content, otherwise your modifications will be overwritten in the next generation.")
25
+ }
26
+
27
+ const utils = {
28
+ camel: (str: string) => camel(str).replaceAll("-", "").replaceAll("_", ""),
29
+ hyphen: (str: string) => hyphen(str).replaceAll("_", "")
30
+ }
31
+
32
+ // Write a basic framework to ensure that there are no errors when reading later
33
+ const apiSchemaSkeleton = `
34
+ export default {
35
+ apiValidator: {},
36
+ apiMethodsSchema: {},
37
+ apiMethodsTypeSchema: {},
38
+ }
39
+ `
40
+ await writeFile(join(cwd(), "generated", "api-schema.ts"), ejs.render(apiSchemaSkeleton, { utils }))
41
+
42
+ // Generate api-schema.ts file through templates
43
+ const templateVars = {
44
+ utils,
45
+ apiPaths: [] as Array<string>,
46
+ apiTestPaths: [] as Array<string>
47
+ }
48
+
49
+ const glob = new Glob("**/*.ts")
50
+ const appFiles = await Array.fromAsync(glob.scan({ cwd: join(cwd(), "src", "apps") }))
51
+
52
+
53
+ console.time(`🧊 File Stage`)
54
+
55
+
56
+ for (const path of appFiles) {
57
+ if (!path.endsWith(".ts")) continue
58
+ const module = await import(/* @vite-ignore */ join(cwd(), "src", "apps", path))
59
+
60
+ if (module?.api?.isApi === true) {
61
+ // Exclude disallowed characters
62
+ if (path.includes("_")) {
63
+ console.error(`\n\nPath: ` + `"${path}"`)
64
+ console.error(`Do not use "_" in the path. If you want to add a separator between words, please use "-".\n`)
65
+ exit(1)
66
+ }
67
+ if (!/^[a-z0-9/-]+$/.test(path.slice(0, -3))) {
68
+ console.error(`\n\nPath: ` + `"${path}"`)
69
+ console.error(`The path can only contain lowercase letters, numbers, and "-".\n`)
70
+ exit(1)
71
+ }
72
+
73
+ templateVars.apiPaths.push(path)
74
+
75
+ if (module?.test?.isApiTest === true) {
76
+ templateVars.apiTestPaths.push(path)
77
+ }
78
+
79
+ // typia
80
+ const filePath = join(cwd(), "generated", "raw", "apps", path)
81
+ const dirPath = join(cwd(), "generated", "raw", "apps", path).split("/").slice(0, -1).join("/")
82
+ if (!existsSync(dirPath)) {
83
+ mkdirSync(dirPath, { recursive: true })
84
+ }
85
+ let importPath = "../../../"
86
+
87
+ for (let i = 0; i < path.split("/").length - 1; i++) {
88
+ importPath = importPath + "../"
89
+ }
90
+ importPath = importPath + "src/apps"
91
+ const template = `
92
+ import typia from "typia";
93
+ import { _validate, type ExecuteResultSuccess } from "milkio";
94
+ import { type TSONEncode } from "@southern-aurora/tson";
95
+ import type * as <%= utils.camel(path.slice(0, -3).replaceAll('/', '$')) %> from '${importPath}/<%= path.slice(0, -3) %>';
96
+
97
+ type ParamsT = Parameters<typeof <%= utils.camel(path.replaceAll('/', '$').slice(0, -${3})) %>['api']['action']>[0];
98
+ export const params = async (params: any) => typia.misc.validatePrune<ParamsT>(params);
99
+ type ResultsT = Awaited<ReturnType<typeof <%= utils.camel(path.replaceAll('/', '$').slice(0, -${3})) %>['api']['action']>>;
100
+ export const results = async (results: any) => { _validate(typia.validate<TSONEncode<ExecuteResultSuccess<ResultsT>>>(results)); return typia.json.stringify<TSONEncode<ExecuteResultSuccess<ResultsT>>>(results); };
101
+
102
+ `.trim()
103
+ // export const paramsSchema = typia.json.application<[{ data: ParamsT }], "swagger">();
104
+
105
+ await writeFile(filePath, ejs.render(template, { ...templateVars, path }))
106
+ }
107
+ }
108
+
109
+ await writeFile(
110
+ join(cwd(), "generated", "api-schema.ts"),
111
+ ejs.render(
112
+ `
113
+ /**
114
+ * ⚠️ This file is generated and modifications will be overwritten
115
+ */
116
+
117
+ // api
118
+ <% for (const path of ${"apiPaths"}) { %>import type * as <%= utils.camel(path.slice(0, -3).replaceAll('/', '$')) %> from '${"../src/apps"}/<%= path.slice(0, -3) %>'
119
+ <% } %>
120
+ import _apiValidator from './products/api-validator.ts'
121
+
122
+ export default {
123
+ apiValidator: _apiValidator,
124
+ ${"apiMethodsSchema"}: {
125
+ <% for (const path of apiPaths) { %>'<%= utils.hyphen(path.slice(0, -${3})) %>': () => ({ module: import('../src/apps/<%= path.slice(0, -${3}) %>') }),
126
+ <% } %>
127
+ },
128
+ ${"apiMethodsTypeSchema"}: {
129
+ <% for (const path of apiPaths) { %>'<%= utils.hyphen(path.slice(0, -${3})) %>': undefined as unknown as typeof <%= utils.camel(path.slice(0, -${3}).replaceAll('/', '$')) %>,
130
+ <% } %>
131
+ },
132
+ ${"apiTestsSchema"}: {
133
+ <% for (const path of apiTestPaths) { %>'<%= utils.hyphen(path.slice(0, -${3})) %>': () => ({ module: import('../src/apps/<%= path.slice(0, -${3}) %>') }),
134
+ <% } %>
135
+ },
136
+ }
137
+ `.trim(),
138
+ templateVars
139
+ )
140
+ )
141
+
142
+ // api
143
+ const apiValidatorTemplate = `/**
144
+ * ⚠️This file is generated and modifications will be overwritten
145
+ */
146
+
147
+ export default {
148
+ generatedAt: ${new Date().getTime()},
149
+ ${"validate"}: {
150
+ <% for (const path of apiPaths) { %>'<%= utils.hyphen(path.slice(0, -${3})) %>': () => import('./apps/<%= utils.hyphen(path) %>'),
151
+ <% } %>
152
+ },
153
+ }
154
+ `.trim()
155
+ await writeFile(join(cwd(), "generated", "raw", "api-validator.ts"), ejs.render(apiValidatorTemplate, templateVars))
156
+
157
+ console.timeEnd(`🧊 File Stage`)
158
+ console.log(``)
159
+
160
+ // typia
161
+ console.time(`🧊 Typia Stage`)
162
+ await $`bun run ./node_modules/typia/lib/executable/typia.js generate --input generated/raw --output generated/products --project tsconfig.json`
163
+ console.timeEnd(`🧊 Typia Stage`)
164
+ console.log(``)
165
+
166
+ if (!existsSync(join(cwd(), "milkio.toml"))) return
167
+ const milkioConfig = (await import(join(cwd(), "milkio.toml"))).default as MilkioConfig
168
+ if (!milkioConfig?.generate?.significant) return
169
+ let i = 0
170
+ for (const command of milkioConfig.generate.significant) {
171
+ ++i
172
+ console.time(`🧊 Significant Stage (LINE ${i})`)
173
+ await $`${{ raw: command }}`
174
+ console.timeEnd(`🧊 Significant Stage (LINE ${i})`)
175
+ }
176
+ }