sonamu 0.11.11 → 0.11.13

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 (44) hide show
  1. package/dist/code-formatting/index.d.ts +8 -0
  2. package/dist/code-formatting/index.d.ts.map +1 -0
  3. package/dist/code-formatting/index.js +27 -0
  4. package/dist/code-formatting/oxfmt.d.ts +5 -0
  5. package/dist/code-formatting/oxfmt.d.ts.map +1 -0
  6. package/dist/code-formatting/oxfmt.js +61 -0
  7. package/dist/code-formatting/oxlint.d.ts +30 -0
  8. package/dist/code-formatting/oxlint.d.ts.map +1 -0
  9. package/dist/code-formatting/oxlint.js +130 -0
  10. package/dist/cone/cone-generator.js +1 -1
  11. package/dist/dict/sonamu-dictionary.js +3 -3
  12. package/dist/entity/entity.d.ts.map +1 -1
  13. package/dist/entity/entity.js +3 -3
  14. package/dist/migration/code-generation.d.ts.map +1 -1
  15. package/dist/migration/code-generation.js +3 -3
  16. package/dist/syncer/code-generator.d.ts.map +1 -1
  17. package/dist/syncer/code-generator.js +3 -3
  18. package/dist/syncer/file-copy.js +3 -3
  19. package/dist/syncer/filesystem-dependencies.js +1 -1
  20. package/dist/syncer/syncer-actions.d.ts.map +1 -1
  21. package/dist/syncer/syncer-actions.js +3 -3
  22. package/dist/template/implementations/sd.template.js +1 -1
  23. package/dist/tooling/cdd-service.js +1 -1
  24. package/dist/ui-web/assets/index-BMdIyKXS.css +1 -0
  25. package/dist/ui-web/assets/{index-C_RUxOdN.js → index-ClxAM2Lk.js} +37 -37
  26. package/dist/ui-web/index.html +2 -2
  27. package/dist/utils/fs-utils.js +3 -3
  28. package/package.json +3 -3
  29. package/src/code-formatting/__tests__/formatter-batching.test.ts +210 -0
  30. package/src/{utils → code-formatting}/__tests__/formatter-bin.test.ts +2 -2
  31. package/src/code-formatting/index.ts +29 -0
  32. package/src/code-formatting/oxfmt.ts +63 -0
  33. package/src/{utils/formatter.ts → code-formatting/oxlint.ts} +67 -95
  34. package/src/dict/sonamu-dictionary.ts +1 -1
  35. package/src/entity/entity.ts +1 -1
  36. package/src/migration/code-generation.ts +1 -1
  37. package/src/syncer/code-generator.ts +1 -1
  38. package/src/syncer/file-copy.ts +1 -1
  39. package/src/syncer/syncer-actions.ts +1 -1
  40. package/src/utils/fs-utils.ts +1 -1
  41. package/dist/ui-web/assets/index-5i4uZVES.css +0 -1
  42. package/dist/utils/formatter.d.ts +0 -14
  43. package/dist/utils/formatter.d.ts.map +0 -1
  44. package/dist/utils/formatter.js +0 -147
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/sonamu-ui/setting.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>{{projectName}}: Sonamu UI</title>
8
- <script type="module" crossorigin src="/sonamu-ui/assets/index-C_RUxOdN.js"></script>
9
- <link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-5i4uZVES.css">
8
+ <script type="module" crossorigin src="/sonamu-ui/assets/index-ClxAM2Lk.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-BMdIyKXS.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
@@ -1,5 +1,5 @@
1
1
  import { __esmMin } from "../_virtual/_rolldown/runtime.js";
2
- import { formatCode, init_formatter } from "./formatter.js";
2
+ import { formatCode, init_code_formatting } from "../code-formatting/index.js";
3
3
  import { constants } from "fs";
4
4
  import path, { dirname } from "path";
5
5
  import { access, readFile, stat, writeFile } from "fs/promises";
@@ -63,10 +63,10 @@ async function copyFileWithReplaceCoreToShared(fromPath, toPath, syncHeader) {
63
63
  return true;
64
64
  }
65
65
  var init_fs_utils = __esmMin((() => {
66
- init_formatter();
66
+ init_code_formatting();
67
67
  }));
68
68
  //#endregion
69
69
  init_fs_utils();
70
70
  export { copyFileWithReplaceCoreToShared, exists, fileExists, init_fs_utils };
71
71
 
72
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZnMtdXRpbHMuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL2ZzLXV0aWxzLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNvbnN0YW50cyB9IGZyb20gXCJmc1wiO1xuaW1wb3J0IHsgdHlwZSBQYXRoTGlrZSB9IGZyb20gXCJmc1wiO1xuaW1wb3J0IHsgYWNjZXNzLCByZWFkRmlsZSwgc3RhdCwgd3JpdGVGaWxlIH0gZnJvbSBcImZzL3Byb21pc2VzXCI7XG5pbXBvcnQgcGF0aCwgeyBkaXJuYW1lIH0gZnJvbSBcInBhdGhcIjtcblxuaW1wb3J0IHsgZm9ybWF0Q29kZSB9IGZyb20gXCIuL2Zvcm1hdHRlclwiO1xuXG4vKipcbiAqIGZzL3Byb21pc2Vz7JeQ64qUIGV4aXN0c+qwgCDsl4bslrTsmpQuIOuMgOyLoCBhY2Nlc3PqsIAg7J6I7Iq164uI64ukLlxuICog6re8642wIOyWmOuKlCDsnbjthLDtjpjsnbTsiqTqsIAg7JOw6riwIOu2iO2OuO2VtOyalC4g6re4656Y7IScIOqwkOyLuOyjvOyXiOyKteuLiOuLpC5cbiAqIEBwYXJhbSB0YXJnZXRQYXRoXG4gKiBAcmV0dXJuc1xuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gZXhpc3RzKHRhcmdldFBhdGg6IFBhdGhMaWtlKTogUHJvbWlzZTxib29sZWFuPiB7XG4gIHRyeSB7XG4gICAgYXdhaXQgYWNjZXNzKHRhcmdldFBhdGgsIGNvbnN0YW50cy5GX09LKTtcbiAgICByZXR1cm4gdHJ1ZTtcbiAgfSBjYXRjaCB7XG4gICAgcmV0dXJuIGZhbHNlO1xuICB9XG59XG5cbi8vIOuUlOugie2GoOumrOqwgCDslYTri4jrnbwg7YyM7J2866eMIOyhtOyerO2VmOuKlOyngCDtmZXsnbjtlanri4jri6QuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gZmlsZUV4aXN0cyh0YXJnZXRQYXRoOiBQYXRoTGlrZSk6IFByb21pc2U8Ym9vbGVhbj4ge1xuICB0cnkge1xuICAgIGNvbnN0IHN0YXRzID0gYXdhaXQgc3RhdCh0YXJnZXRQYXRoKTtcbiAgICByZXR1cm4gc3RhdHMuaXNGaWxlKCk7XG4gIH0gY2F0Y2gge1xuICAgIHJldHVybiBmYWxzZTtcbiAgfVxufVxuXG4vKipcbiAqIO2MjOydvOydhCDrs7XsgqztlZjrqbTshJwgYGZyb20gXCJzb25hbXVcImAgaW1wb3J066W8IHNvbmFtdS5zaGFyZWQudHMg6rK966Gc66GcIOy5mO2ZmO2VqeuLiOuLpC5cbiAqXG4gKiB3ZWLsnbTrgpggYXBwIOuTseyXkOuKlCBzb25hbXUg7Yyo7YKk7KeA6rCAIOyXhuycvOuvgOuhnCwg7ZWo6ruYIOuzteyCrOuQmOuKlCBzb25hbXUuc2hhcmVkLnRz7JeQ7IScIOqwgOyguOyYpOuPhOuhnSDrs4DtmZjtlanri4jri6QuXG4gKiDrjIDsg4Eg7YyM7J287J2YIOychOy5mOyXkCDrlLDrnbwg7IOB64yAIOqyveuhnOqwgCDri6zrnbzsp5Hri4jri6Q6XG4gKiAtIHNlcnZpY2VzL3NvbmFtdS5nZW5lcmF0ZWQudHMg4oaSIC4vc29uYW11LnNoYXJlZFxuICogLSBzZXJ2aWNlcy91c2VyL3VzZXIudHlwZXMudHMg4oaSIC4uL3NvbmFtdS5zaGFyZWRcbiAqIC0gaTE4bi9rby50cyDihpIgLi4vc2VydmljZXMvc29uYW11LnNoYXJlZFxuICpcbiAqIHRzL3RzeCDtjIzsnbzsnYAg7JOw6riwIOyghOyXkCBveGZtdC9veGxpbnQg7ZWc67KIIOuPjOugpOykjeuLiOuLpCFcbiAqXG4gKiBAcGFyYW0gZnJvbVBhdGgg7JuQ67O4IO2MjOydvCDqsr3roZxcbiAqIEBwYXJhbSB0b1BhdGgg64yA7IOBIO2MjOydvCDqsr3roZxcbiAqIEBwYXJhbSBzeW5jSGVhZGVyIOuPmeq4sO2ZlCDsi5wg7YyM7J28IOy1nOyDgeuLqOyXkCDsgr3snoXtlaAg7KO87ISdIOu4lOuhnS4g6riw7KG0IEBnZW5lcmF0ZWQg67iU66Gd7J20IOyeiOycvOuptCDqtZDssrTtlZjqs6AsIOyXhuycvOuptCDstZzsg4Hri6jsl5Ag7LaU6rCA7ZWp64uI64ukLlxuICogQHJldHVybnMg7YyM7J287J2EIOyNvOycvOuptCB0cnVlLCDqsbTrhIjrm7Dsl4jsnLzrqbQgZmFsc2VcbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNvcHlGaWxlV2l0aFJlcGxhY2VDb3JlVG9TaGFyZWQoXG4gIGZyb21QYXRoOiBzdHJpbmcsXG4gIHRvUGF0aDogc3RyaW5nLFxuICBzeW5jSGVhZGVyPzogc3RyaW5nLFxuKTogUHJvbWlzZTxib29sZWFuPiB7XG4gIGlmICghKGF3YWl0IGV4aXN0cyhmcm9tUGF0aCkpKSB7XG4gICAgcmV0dXJuIGZhbHNlO1xuICB9XG5cbiAgY29uc3Qgb2xkRmlsZUNvbnRlbnQgPSAoYXdhaXQgcmVhZEZpbGUoZnJvbVBhdGgpKS50b1N0cmluZygpO1xuXG4gIGxldCBuZXdGaWxlQ29udGVudCA9ICgoKSA9PiB7XG4gICAgLy8gc29uYW11LnNoYXJlZC50c+uKlCDtla3sg4Ege2Jhc2V9L3NyYy9zZXJ2aWNlcy9zb25hbXUuc2hhcmVkLnRz7JeQIOychOy5mO2VqeuLiOuLpC5cbiAgICAvLyB0b1BhdGjsl5DshJwgL3NyYy/rpbwg7LC+7JWEIHNlcnZpY2VzIOuUlOugie2GoOumrCDqsr3roZzrpbwg6rOE7IKw7ZWp64uI64ukLlxuICAgIGNvbnN0IHNyY01hdGNoID0gdG9QYXRoLm1hdGNoKC9eKC4rXFwvc3JjKVxcLy8pO1xuICAgIGlmICghc3JjTWF0Y2gpIHtcbiAgICAgIC8vIC9zcmMv6rCAIOyXhuycvOuptCDrs4DtmZgg7JeG7J20IOq3uOuMgOuhnCDrsJjtmZhcbiAgICAgIHJldHVybiBvbGRGaWxlQ29udGVudDtcbiAgICB9XG5cbiAgICBjb25zdCBzZXJ2aWNlc0RpciA9IHBhdGguam9pbihzcmNNYXRjaFsxXSwgXCJzZXJ2aWNlc1wiKTtcbiAgICBjb25zdCBmaWxlRGlyID0gZGlybmFtZSh0b1BhdGgpO1xuICAgIGNvbnN0IHJlbGF0aXZlUGF0aCA9IHBhdGgucmVsYXRpdmUoZmlsZURpciwgc2VydmljZXNEaXIpO1xuICAgIGNvbnN0IHNoYXJlZFBhdGggPSByZWxhdGl2ZVBhdGggPT09IFwiXCIgPyBcIi4vc29uYW11LnNoYXJlZFwiIDogYCR7cmVsYXRpdmVQYXRofS9zb25hbXUuc2hhcmVkYDtcblxuICAgIHJldHVybiBvbGRGaWxlQ29udGVudC5yZXBsYWNlKC9mcm9tIFwic29uYW11KFxcL2RpY3QpP1wiL2csIGBmcm9tIFwiJHtzaGFyZWRQYXRofVwiYCk7XG4gIH0pKCk7XG5cbiAgLy8gc3luY0hlYWRlcuqwgCDsoJzqs7XrkJwg6rK97JqwIEBnZW5lcmF0ZWQg67iU66Gd7J2EIOq1kOyytO2VmOqxsOuCmCDstZzsg4Hri6jsl5Ag7LaU6rCA7ZWp64uI64ukLlxuICAvLyBgKi9gIOynge2bhOyXkOuKlCDtla3sg4Eg67mIIOykhCDtlZwg6rCc66W8IOuztOyepe2VmOyXrCBveGZtdOqwgCDso7zshJ3snYQgaW1wb3J07J2YIGxlYWRpbmcgY29tbWVudOuhnCDsmKTsnbjtlbQg7KCV66CsIOyLnCDrgYzroKTqsIDripQg66y47KCc66W8IOuwqeyngO2VqeuLiOuLpC5cbiAgaWYgKHN5bmNIZWFkZXIpIHtcbiAgICAvLyDquLDsobQgQGdlbmVyYXRlZCDruJTroZ0gKyDrkqTsnbTslrQg7Jik64qUIOu5iCDspITrk6Qo7J6I7Jy866m0IO2PrO2VqCnsnYQg7ZWc6rq867KI7JeQIOunpOy5re2VmOyXrCDqtZDssrQg7IucIOu5iCDspITsnbQg64iE7KCB65CY7KeAIOyViuqyjCDtlanri4jri6QuXG4gICAgLy8g7ZWcIOykhOynnOumrCgvKiogQGdlbmVyYXRlZCAqLynripQg66ek7Lmt7ZWY7KeAIOyViuycvOuvgOuhnCDsiJjrj5nsnLzroZwg7LaV7JW97ZWY7KeAIOuniOyEuOyalC5cbiAgICBjb25zdCBnZW5lcmF0ZWRCbG9ja1JlZ2V4ID0gL1xcL1xcKlxcKlxccj9cXG4gXFwqIEBnZW5lcmF0ZWRcXHI/XFxuW1xcc1xcU10qP1xcKlxcL1xccj9cXG4oXFxyP1xcbikqLztcbiAgICBpZiAoZ2VuZXJhdGVkQmxvY2tSZWdleC50ZXN0KG5ld0ZpbGVDb250ZW50KSkge1xuICAgICAgbmV3RmlsZUNvbnRlbnQgPSBuZXdGaWxlQ29udGVudC5yZXBsYWNlKGdlbmVyYXRlZEJsb2NrUmVnZXgsIGAke3N5bmNIZWFkZXJ9XFxuXFxuYCk7XG4gICAgfSBlbHNlIHtcbiAgICAgIG5ld0ZpbGVDb250ZW50ID0gYCR7c3luY0hlYWRlcn1cXG5cXG4ke25ld0ZpbGVDb250ZW50fWA7XG4gICAgfVxuICB9XG5cbiAgLy8gLnRzLy50c3gg7IKw7Lac66y87J2AIOyTsOq4sCDsoITsl5Ag7Y+s66e364+EIO2VtOykmOyalCDjhY7jhY5cbiAgaWYgKHRvUGF0aC5lbmRzV2l0aChcIi50c1wiKSB8fCB0b1BhdGguZW5kc1dpdGgoXCIudHN4XCIpKSB7XG4gICAgbmV3RmlsZUNvbnRlbnQgPSBhd2FpdCBmb3JtYXRDb2RlKG5ld0ZpbGVDb250ZW50LCB0b1BhdGgpO1xuICB9XG5cbiAgYXdhaXQgd3JpdGVGaWxlKHRvUGF0aCwgbmV3RmlsZUNvbnRlbnQpO1xuICByZXR1cm4gdHJ1ZTtcbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7O0FBYUEsZUFBc0IsT0FBTyxZQUF3QztDQUNuRSxJQUFJO0VBQ0YsTUFBTSxPQUFPLFlBQVksVUFBVSxJQUFJO0VBQ3ZDLE9BQU87Q0FDVCxRQUFRO0VBQ04sT0FBTztDQUNUO0FBQ0Y7QUFHQSxlQUFzQixXQUFXLFlBQXdDO0NBQ3ZFLElBQUk7RUFFRixRQUFPLE1BRGEsS0FBSyxVQUFVLEVBQzVCLENBQU0sT0FBTztDQUN0QixRQUFRO0VBQ04sT0FBTztDQUNUO0FBQ0Y7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBa0JBLGVBQXNCLGdDQUNwQixVQUNBLFFBQ0EsWUFDa0I7Q0FDbEIsSUFBSSxDQUFFLE1BQU0sT0FBTyxRQUFRLEdBQ3pCLE9BQU87Q0FHVCxNQUFNLGtCQUFrQixNQUFNLFNBQVMsUUFBUSxFQUFBLENBQUcsU0FBUztDQUUzRCxJQUFJLHdCQUF3QjtFQUcxQixNQUFNLFdBQVcsT0FBTyxNQUFNLGNBQWM7RUFDNUMsSUFBSSxDQUFDLFVBRUgsT0FBTztFQUdULE1BQU0sY0FBYyxLQUFLLEtBQUssU0FBUyxJQUFJLFVBQVU7RUFDckQsTUFBTSxVQUFVLFFBQVEsTUFBTTtFQUM5QixNQUFNLGVBQWUsS0FBSyxTQUFTLFNBQVMsV0FBVztFQUN2RCxNQUFNLGFBQWEsaUJBQWlCLEtBQUssb0JBQW9CLEdBQUcsYUFBYTtFQUU3RSxPQUFPLGVBQWUsUUFBUSwyQkFBMkIsU0FBUyxXQUFXLEVBQUU7Q0FDakYsRUFBQSxDQUFHO0NBSUgsSUFBSSxZQUFZO0VBR2QsTUFBTSxzQkFBc0I7RUFDNUIsSUFBSSxvQkFBb0IsS0FBSyxjQUFjLEdBQ3pDLGlCQUFpQixlQUFlLFFBQVEscUJBQXFCLEdBQUcsV0FBVyxLQUFLO09BRWhGLGlCQUFpQixHQUFHLFdBQVcsTUFBTTtDQUV6QztDQUdBLElBQUksT0FBTyxTQUFTLEtBQUssS0FBSyxPQUFPLFNBQVMsTUFBTSxHQUNsRCxpQkFBaUIsTUFBTSxXQUFXLGdCQUFnQixNQUFNO0NBRzFELE1BQU0sVUFBVSxRQUFRLGNBQWM7Q0FDdEMsT0FBTztBQUNUOztnQkEzRndDIn0=
72
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZnMtdXRpbHMuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL2ZzLXV0aWxzLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNvbnN0YW50cyB9IGZyb20gXCJmc1wiO1xuaW1wb3J0IHsgdHlwZSBQYXRoTGlrZSB9IGZyb20gXCJmc1wiO1xuaW1wb3J0IHsgYWNjZXNzLCByZWFkRmlsZSwgc3RhdCwgd3JpdGVGaWxlIH0gZnJvbSBcImZzL3Byb21pc2VzXCI7XG5pbXBvcnQgcGF0aCwgeyBkaXJuYW1lIH0gZnJvbSBcInBhdGhcIjtcblxuaW1wb3J0IHsgZm9ybWF0Q29kZSB9IGZyb20gXCIuLi9jb2RlLWZvcm1hdHRpbmdcIjtcblxuLyoqXG4gKiBmcy9wcm9taXNlc+yXkOuKlCBleGlzdHPqsIAg7JeG7Ja07JqULiDrjIDsi6AgYWNjZXNz6rCAIOyeiOyKteuLiOuLpC5cbiAqIOq3vOuNsCDslpjripQg7J247YSw7Y6Y7J207Iqk6rCAIOyTsOq4sCDrtojtjrjtlbTsmpQuIOq3uOuemOyEnCDqsJDsi7jso7zsl4jsirXri4jri6QuXG4gKiBAcGFyYW0gdGFyZ2V0UGF0aFxuICogQHJldHVybnNcbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGV4aXN0cyh0YXJnZXRQYXRoOiBQYXRoTGlrZSk6IFByb21pc2U8Ym9vbGVhbj4ge1xuICB0cnkge1xuICAgIGF3YWl0IGFjY2Vzcyh0YXJnZXRQYXRoLCBjb25zdGFudHMuRl9PSyk7XG4gICAgcmV0dXJuIHRydWU7XG4gIH0gY2F0Y2gge1xuICAgIHJldHVybiBmYWxzZTtcbiAgfVxufVxuXG4vLyDrlJTroInthqDrpqzqsIAg7JWE64uI6528IO2MjOydvOunjCDsobTsnqztlZjripTsp4Ag7ZmV7J247ZWp64uI64ukLlxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGZpbGVFeGlzdHModGFyZ2V0UGF0aDogUGF0aExpa2UpOiBQcm9taXNlPGJvb2xlYW4+IHtcbiAgdHJ5IHtcbiAgICBjb25zdCBzdGF0cyA9IGF3YWl0IHN0YXQodGFyZ2V0UGF0aCk7XG4gICAgcmV0dXJuIHN0YXRzLmlzRmlsZSgpO1xuICB9IGNhdGNoIHtcbiAgICByZXR1cm4gZmFsc2U7XG4gIH1cbn1cblxuLyoqXG4gKiDtjIzsnbzsnYQg67O17IKs7ZWY66m07IScIGBmcm9tIFwic29uYW11XCJgIGltcG9ydOulvCBzb25hbXUuc2hhcmVkLnRzIOqyveuhnOuhnCDsuZjtmZjtlanri4jri6QuXG4gKlxuICogd2Vi7J2064KYIGFwcCDrk7Hsl5DripQgc29uYW11IO2MqO2CpOyngOqwgCDsl4bsnLzrr4DroZwsIO2VqOq7mCDrs7XsgqzrkJjripQgc29uYW11LnNoYXJlZC50c+yXkOyEnCDqsIDsoLjsmKTrj4TroZ0g67OA7ZmY7ZWp64uI64ukLlxuICog64yA7IOBIO2MjOydvOydmCDsnITsuZjsl5Ag65Sw6528IOyDgeuMgCDqsr3roZzqsIAg64us65287KeR64uI64ukOlxuICogLSBzZXJ2aWNlcy9zb25hbXUuZ2VuZXJhdGVkLnRzIOKGkiAuL3NvbmFtdS5zaGFyZWRcbiAqIC0gc2VydmljZXMvdXNlci91c2VyLnR5cGVzLnRzIOKGkiAuLi9zb25hbXUuc2hhcmVkXG4gKiAtIGkxOG4va28udHMg4oaSIC4uL3NlcnZpY2VzL3NvbmFtdS5zaGFyZWRcbiAqXG4gKiB0cy90c3gg7YyM7J287J2AIOyTsOq4sCDsoITsl5Agb3hmbXQvb3hsaW50IO2VnOuyiCDrj4zroKTspI3ri4jri6QhXG4gKlxuICogQHBhcmFtIGZyb21QYXRoIOybkOuzuCDtjIzsnbwg6rK966GcXG4gKiBAcGFyYW0gdG9QYXRoIOuMgOyDgSDtjIzsnbwg6rK966GcXG4gKiBAcGFyYW0gc3luY0hlYWRlciDrj5nquLDtmZQg7IucIO2MjOydvCDstZzsg4Hri6jsl5Ag7IK97J6F7ZWgIOyjvOyEnSDruJTroZ0uIOq4sOyhtCBAZ2VuZXJhdGVkIOu4lOuhneydtCDsnojsnLzrqbQg6rWQ7LK07ZWY6rOgLCDsl4bsnLzrqbQg7LWc7IOB64uo7JeQIOy2lOqwgO2VqeuLiOuLpC5cbiAqIEByZXR1cm5zIO2MjOydvOydhCDsjbzsnLzrqbQgdHJ1ZSwg6rG064SI65uw7JeI7Jy866m0IGZhbHNlXG4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBjb3B5RmlsZVdpdGhSZXBsYWNlQ29yZVRvU2hhcmVkKFxuICBmcm9tUGF0aDogc3RyaW5nLFxuICB0b1BhdGg6IHN0cmluZyxcbiAgc3luY0hlYWRlcj86IHN0cmluZyxcbik6IFByb21pc2U8Ym9vbGVhbj4ge1xuICBpZiAoIShhd2FpdCBleGlzdHMoZnJvbVBhdGgpKSkge1xuICAgIHJldHVybiBmYWxzZTtcbiAgfVxuXG4gIGNvbnN0IG9sZEZpbGVDb250ZW50ID0gKGF3YWl0IHJlYWRGaWxlKGZyb21QYXRoKSkudG9TdHJpbmcoKTtcblxuICBsZXQgbmV3RmlsZUNvbnRlbnQgPSAoKCkgPT4ge1xuICAgIC8vIHNvbmFtdS5zaGFyZWQudHPripQg7ZWt7IOBIHtiYXNlfS9zcmMvc2VydmljZXMvc29uYW11LnNoYXJlZC50c+yXkCDsnITsuZjtlanri4jri6QuXG4gICAgLy8gdG9QYXRo7JeQ7IScIC9zcmMv66W8IOywvuyVhCBzZXJ2aWNlcyDrlJTroInthqDrpqwg6rK966Gc66W8IOqzhOyCsO2VqeuLiOuLpC5cbiAgICBjb25zdCBzcmNNYXRjaCA9IHRvUGF0aC5tYXRjaCgvXiguK1xcL3NyYylcXC8vKTtcbiAgICBpZiAoIXNyY01hdGNoKSB7XG4gICAgICAvLyAvc3JjL+qwgCDsl4bsnLzrqbQg67OA7ZmYIOyXhuydtCDqt7jrjIDroZwg67CY7ZmYXG4gICAgICByZXR1cm4gb2xkRmlsZUNvbnRlbnQ7XG4gICAgfVxuXG4gICAgY29uc3Qgc2VydmljZXNEaXIgPSBwYXRoLmpvaW4oc3JjTWF0Y2hbMV0sIFwic2VydmljZXNcIik7XG4gICAgY29uc3QgZmlsZURpciA9IGRpcm5hbWUodG9QYXRoKTtcbiAgICBjb25zdCByZWxhdGl2ZVBhdGggPSBwYXRoLnJlbGF0aXZlKGZpbGVEaXIsIHNlcnZpY2VzRGlyKTtcbiAgICBjb25zdCBzaGFyZWRQYXRoID0gcmVsYXRpdmVQYXRoID09PSBcIlwiID8gXCIuL3NvbmFtdS5zaGFyZWRcIiA6IGAke3JlbGF0aXZlUGF0aH0vc29uYW11LnNoYXJlZGA7XG5cbiAgICByZXR1cm4gb2xkRmlsZUNvbnRlbnQucmVwbGFjZSgvZnJvbSBcInNvbmFtdShcXC9kaWN0KT9cIi9nLCBgZnJvbSBcIiR7c2hhcmVkUGF0aH1cImApO1xuICB9KSgpO1xuXG4gIC8vIHN5bmNIZWFkZXLqsIAg7KCc6rO165CcIOqyveyasCBAZ2VuZXJhdGVkIOu4lOuhneydhCDqtZDssrTtlZjqsbDrgpgg7LWc7IOB64uo7JeQIOy2lOqwgO2VqeuLiOuLpC5cbiAgLy8gYCovYCDsp4Htm4Tsl5DripQg7ZWt7IOBIOu5iCDspIQg7ZWcIOqwnOulvCDrs7TsnqXtlZjsl6wgb3hmbXTqsIAg7KO87ISd7J2EIGltcG9ydOydmCBsZWFkaW5nIGNvbW1lbnTroZwg7Jik7J247ZW0IOygleugrCDsi5wg64GM66Ck6rCA64qUIOusuOygnOulvCDrsKnsp4Dtlanri4jri6QuXG4gIGlmIChzeW5jSGVhZGVyKSB7XG4gICAgLy8g6riw7KG0IEBnZW5lcmF0ZWQg67iU66GdICsg65Kk7J207Ja0IOyYpOuKlCDruYgg7KSE65OkKOyeiOycvOuptCDtj6ztlagp7J2EIO2VnOq6vOuyiOyXkCDrp6Tsua3tlZjsl6wg6rWQ7LK0IOyLnCDruYgg7KSE7J20IOuIhOyggeuQmOyngCDslYrqsowg7ZWp64uI64ukLlxuICAgIC8vIO2VnCDspITsp5zrpqwoLyoqIEBnZW5lcmF0ZWQgKi8p64qUIOunpOy5re2VmOyngCDslYrsnLzrr4DroZwg7IiY64+Z7Jy866GcIOy2leyVve2VmOyngCDrp4jshLjsmpQuXG4gICAgY29uc3QgZ2VuZXJhdGVkQmxvY2tSZWdleCA9IC9cXC9cXCpcXCpcXHI/XFxuIFxcKiBAZ2VuZXJhdGVkXFxyP1xcbltcXHNcXFNdKj9cXCpcXC9cXHI/XFxuKFxccj9cXG4pKi87XG4gICAgaWYgKGdlbmVyYXRlZEJsb2NrUmVnZXgudGVzdChuZXdGaWxlQ29udGVudCkpIHtcbiAgICAgIG5ld0ZpbGVDb250ZW50ID0gbmV3RmlsZUNvbnRlbnQucmVwbGFjZShnZW5lcmF0ZWRCbG9ja1JlZ2V4LCBgJHtzeW5jSGVhZGVyfVxcblxcbmApO1xuICAgIH0gZWxzZSB7XG4gICAgICBuZXdGaWxlQ29udGVudCA9IGAke3N5bmNIZWFkZXJ9XFxuXFxuJHtuZXdGaWxlQ29udGVudH1gO1xuICAgIH1cbiAgfVxuXG4gIC8vIC50cy8udHN4IOyCsOy2nOusvOydgCDsk7DquLAg7KCE7JeQIO2PrOunt+uPhCDtlbTspJjsmpQg44WO44WOXG4gIGlmICh0b1BhdGguZW5kc1dpdGgoXCIudHNcIikgfHwgdG9QYXRoLmVuZHNXaXRoKFwiLnRzeFwiKSkge1xuICAgIG5ld0ZpbGVDb250ZW50ID0gYXdhaXQgZm9ybWF0Q29kZShuZXdGaWxlQ29udGVudCwgdG9QYXRoKTtcbiAgfVxuXG4gIGF3YWl0IHdyaXRlRmlsZSh0b1BhdGgsIG5ld0ZpbGVDb250ZW50KTtcbiAgcmV0dXJuIHRydWU7XG59XG4iXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7OztBQWFBLGVBQXNCLE9BQU8sWUFBd0M7Q0FDbkUsSUFBSTtFQUNGLE1BQU0sT0FBTyxZQUFZLFVBQVUsSUFBSTtFQUN2QyxPQUFPO0NBQ1QsUUFBUTtFQUNOLE9BQU87Q0FDVDtBQUNGO0FBR0EsZUFBc0IsV0FBVyxZQUF3QztDQUN2RSxJQUFJO0VBRUYsUUFBTyxNQURhLEtBQUssVUFBVSxFQUM1QixDQUFNLE9BQU87Q0FDdEIsUUFBUTtFQUNOLE9BQU87Q0FDVDtBQUNGOzs7Ozs7Ozs7Ozs7Ozs7OztBQWtCQSxlQUFzQixnQ0FDcEIsVUFDQSxRQUNBLFlBQ2tCO0NBQ2xCLElBQUksQ0FBRSxNQUFNLE9BQU8sUUFBUSxHQUN6QixPQUFPO0NBR1QsTUFBTSxrQkFBa0IsTUFBTSxTQUFTLFFBQVEsRUFBQSxDQUFHLFNBQVM7Q0FFM0QsSUFBSSx3QkFBd0I7RUFHMUIsTUFBTSxXQUFXLE9BQU8sTUFBTSxjQUFjO0VBQzVDLElBQUksQ0FBQyxVQUVILE9BQU87RUFHVCxNQUFNLGNBQWMsS0FBSyxLQUFLLFNBQVMsSUFBSSxVQUFVO0VBQ3JELE1BQU0sVUFBVSxRQUFRLE1BQU07RUFDOUIsTUFBTSxlQUFlLEtBQUssU0FBUyxTQUFTLFdBQVc7RUFDdkQsTUFBTSxhQUFhLGlCQUFpQixLQUFLLG9CQUFvQixHQUFHLGFBQWE7RUFFN0UsT0FBTyxlQUFlLFFBQVEsMkJBQTJCLFNBQVMsV0FBVyxFQUFFO0NBQ2pGLEVBQUEsQ0FBRztDQUlILElBQUksWUFBWTtFQUdkLE1BQU0sc0JBQXNCO0VBQzVCLElBQUksb0JBQW9CLEtBQUssY0FBYyxHQUN6QyxpQkFBaUIsZUFBZSxRQUFRLHFCQUFxQixHQUFHLFdBQVcsS0FBSztPQUVoRixpQkFBaUIsR0FBRyxXQUFXLE1BQU07Q0FFekM7Q0FHQSxJQUFJLE9BQU8sU0FBUyxLQUFLLEtBQUssT0FBTyxTQUFTLE1BQU0sR0FDbEQsaUJBQWlCLE1BQU0sV0FBVyxnQkFBZ0IsTUFBTTtDQUcxRCxNQUFNLFVBQVUsUUFBUSxjQUFjO0NBQ3RDLE9BQU87QUFDVDs7c0JBM0YrQyJ9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonamu",
3
- "version": "0.11.11",
3
+ "version": "0.11.13",
4
4
  "description": "Sonamu — TypeScript Fullstack API Framework",
5
5
  "keywords": [
6
6
  "framework",
@@ -104,7 +104,7 @@
104
104
  "@logtape/pretty": "2.2.2",
105
105
  "@logtape/redaction": "2.2.2",
106
106
  "@sheetkit/node": "^0.5.0",
107
- "@sonamu-kit/hmr-hook": "^0.5.1",
107
+ "@sonamu-kit/hmr-hook": "^0.6.0",
108
108
  "@sonamu-kit/hmr-runner": "^0.2.0",
109
109
  "@sonamu-kit/tasks": "^0.3.1",
110
110
  "@sonamu-kit/ts-loader": "^2.2.0",
@@ -129,7 +129,7 @@
129
129
  "node-cron": "^4.2.1",
130
130
  "node-sql-parser": "^5.2.0",
131
131
  "oxfmt": "^0.55.0",
132
- "oxlint": "1.80.0",
132
+ "oxlint": "1.86.0",
133
133
  "pg": "^8.16.3",
134
134
  "prompts": "^2.4.2",
135
135
  "qs": "^6.14.1",
@@ -0,0 +1,210 @@
1
+ import { access, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import * as processUtils from "../../utils/process-utils";
8
+ import { formatCode } from "../index";
9
+
10
+ const OXLINT_FLAGS = ["--fix", "--fix-suggestions", "--type-aware"];
11
+ const FIXED_SUFFIX = "// oxlint 적용\n";
12
+
13
+ let testDirectory = "";
14
+
15
+ function createGate() {
16
+ let open = () => {};
17
+ const promise = new Promise<void>((resolve) => {
18
+ open = resolve;
19
+ });
20
+ return { open, promise };
21
+ }
22
+
23
+ function extractTemporaryFiles(args: string[]): string[] {
24
+ return args.slice(OXLINT_FLAGS.length);
25
+ }
26
+
27
+ async function listTemporaryFiles(): Promise<string[]> {
28
+ const entries = await readdir(testDirectory, { withFileTypes: true });
29
+ return entries
30
+ .filter(
31
+ (entry) => entry.isFile() && entry.name.startsWith(".sonamu-") && entry.name.endsWith(".ts"),
32
+ )
33
+ .map((entry) => path.join(testDirectory, entry.name));
34
+ }
35
+
36
+ async function waitForTemporaryFiles(count: number): Promise<void> {
37
+ await vi.waitFor(async () => {
38
+ const paths = await listTemporaryFiles();
39
+ expect(paths).toHaveLength(count);
40
+ const contents = await Promise.all(paths.map((filePath) => readFile(filePath, "utf8")));
41
+ contents.forEach((content) => expect(content.length).toBeGreaterThan(0));
42
+ });
43
+
44
+ // 완료된 파일 쓰기의 후속 continuation까지 실행해 대기열 구성을 확정합니다.
45
+ await new Promise<void>((resolve) => setImmediate(resolve));
46
+ }
47
+
48
+ async function applyFixes(args: string[]): Promise<void> {
49
+ await Promise.all(
50
+ extractTemporaryFiles(args).map(async (filePath) => {
51
+ const source = await readFile(filePath, "utf8");
52
+ await writeFile(filePath, `${source}${FIXED_SUFFIX}`, "utf8");
53
+ }),
54
+ );
55
+ }
56
+
57
+ async function expectTemporaryFilesRemoved(filePaths: string[]): Promise<void> {
58
+ await Promise.all(
59
+ filePaths.map(async (filePath) => {
60
+ await expect(access(filePath)).rejects.toMatchObject({ code: "ENOENT" });
61
+ }),
62
+ );
63
+ }
64
+
65
+ beforeEach(async () => {
66
+ vi.restoreAllMocks();
67
+ vi.unstubAllEnvs();
68
+ vi.stubEnv("NODE_ENV", "development");
69
+ testDirectory = await mkdtemp(path.join(os.tmpdir(), "sonamu-formatter-batching-"));
70
+ vi.spyOn(process, "cwd").mockReturnValue(testDirectory);
71
+ });
72
+
73
+ afterEach(async () => {
74
+ vi.restoreAllMocks();
75
+ vi.unstubAllEnvs();
76
+ await rm(testDirectory, { recursive: true, force: true });
77
+ });
78
+
79
+ describe("formatter oxlint 내부 배칭", () => {
80
+ it("동시 호출을 소수 배치로 합치고 한 번에 한 프로세스만 실행하며 호출별 결과를 반환한다", async () => {
81
+ const releaseExecutions = createGate();
82
+ const firstExecutionStarted = createGate();
83
+ const batches: string[][] = [];
84
+ let activeExecutions = 0;
85
+ let maxActiveExecutions = 0;
86
+
87
+ vi.spyOn(processUtils, "execute").mockImplementation(
88
+ async (_bin: string, args: string[], options) => {
89
+ batches.push(extractTemporaryFiles(args));
90
+ activeExecutions += 1;
91
+ maxActiveExecutions = Math.max(maxActiveExecutions, activeExecutions);
92
+ firstExecutionStarted.open();
93
+ try {
94
+ expect(args.slice(0, OXLINT_FLAGS.length)).toEqual(OXLINT_FLAGS);
95
+ expect(options?.timeout).toBe(10_000);
96
+ await releaseExecutions.promise;
97
+ await applyFixes(args);
98
+ return "";
99
+ } finally {
100
+ activeExecutions -= 1;
101
+ }
102
+ },
103
+ );
104
+
105
+ const inputs = Array.from(
106
+ { length: 8 },
107
+ (_, index) => `const formatterBatchingValue${index} = ${index};\n`,
108
+ );
109
+ const results = inputs.map((code, index) => formatCode(code, `generated-${index}.ts`));
110
+
111
+ await firstExecutionStarted.promise;
112
+ await waitForTemporaryFiles(inputs.length);
113
+ releaseExecutions.open();
114
+
115
+ await expect(Promise.all(results)).resolves.toEqual(
116
+ inputs.map((code) => `${code}${FIXED_SUFFIX}`),
117
+ );
118
+
119
+ expect(batches).toHaveLength(2);
120
+ expect(batches.flat()).toHaveLength(inputs.length);
121
+ expect(maxActiveExecutions).toBe(1);
122
+ await expectTemporaryFilesRemoved(batches.flat());
123
+ });
124
+
125
+ it("실패한 배치의 호출자만 거부하고 실행 중 쌓인 다음 배치는 정상 처리한다", async () => {
126
+ const firstExecution = createGate();
127
+ const firstExecutionStarted = createGate();
128
+ const failingExecution = createGate();
129
+ const succeedingExecutions = createGate();
130
+ const secondExecutionStarted = createGate();
131
+ const batchError = Object.assign(new Error("oxlint 실행 실패"), { code: null });
132
+ const batches: string[][] = [];
133
+
134
+ vi.spyOn(processUtils, "execute").mockImplementation(async (_bin: string, args: string[]) => {
135
+ const batchIndex = batches.length;
136
+ batches.push(extractTemporaryFiles(args));
137
+ if (batchIndex === 0) {
138
+ firstExecutionStarted.open();
139
+ await firstExecution.promise;
140
+ await applyFixes(args);
141
+ return "";
142
+ }
143
+ if (batchIndex === 1) {
144
+ secondExecutionStarted.open();
145
+ await failingExecution.promise;
146
+ throw batchError;
147
+ }
148
+ await succeedingExecutions.promise;
149
+ await applyFixes(args);
150
+ return "";
151
+ });
152
+
153
+ const firstCode = "const formatterBatchingFirst = 1;\n";
154
+ const firstResult = formatCode(firstCode, "first-batch.ts");
155
+ await firstExecutionStarted.promise;
156
+ await waitForTemporaryFiles(1);
157
+
158
+ const failedCodes = [
159
+ "const formatterBatchingFailedA = 2;\n",
160
+ "const formatterBatchingFailedB = 3;\n",
161
+ ];
162
+ const failedResults = failedCodes.map((code, index) => formatCode(code, `failed-${index}.ts`));
163
+ const failedOutcomes = Promise.allSettled(failedResults);
164
+ await waitForTemporaryFiles(3);
165
+ firstExecution.open();
166
+ await secondExecutionStarted.promise;
167
+
168
+ const recoveryCode = "const formatterBatchingRecovered = 4;\n";
169
+ const recoveryResult = formatCode(recoveryCode, "recovered.ts");
170
+ await waitForTemporaryFiles(3);
171
+ failingExecution.open();
172
+ succeedingExecutions.open();
173
+
174
+ await expect(firstResult).resolves.toBe(`${firstCode}${FIXED_SUFFIX}`);
175
+ await expect(failedOutcomes).resolves.toEqual(
176
+ failedCodes.map(() => ({ reason: batchError, status: "rejected" })),
177
+ );
178
+ await expect(recoveryResult).resolves.toBe(`${recoveryCode}${FIXED_SUFFIX}`);
179
+
180
+ expect(batches.map((batch) => batch.length)).toEqual([1, 2, 1]);
181
+ await expectTemporaryFilesRemoved(batches.flat());
182
+ });
183
+
184
+ it("숫자 exit code이면 적용된 fix를 반환하고 임시 파일을 정리한다", async () => {
185
+ const lintError = Object.assign(new Error("수정 후 lint 오류가 남음"), { code: 1 });
186
+ let temporaryFile = "";
187
+ const execute = vi.spyOn(processUtils, "execute").mockImplementation(async (_bin, args) => {
188
+ [temporaryFile] = extractTemporaryFiles(args);
189
+ await applyFixes(args);
190
+ throw lintError;
191
+ });
192
+
193
+ const code = "const formatterBatchingNumericExit = 1;\n";
194
+
195
+ await expect(formatCode(code, "numeric-exit.ts")).resolves.toBe(`${code}${FIXED_SUFFIX}`);
196
+ expect(execute).toHaveBeenCalledTimes(1);
197
+ await expectTemporaryFilesRemoved([temporaryFile]);
198
+ });
199
+
200
+ it("테스트 환경에서는 파일이나 프로세스를 사용하지 않고 원본을 반환한다", async () => {
201
+ vi.stubEnv("NODE_ENV", "test");
202
+ const execute = vi.spyOn(processUtils, "execute");
203
+ const code = "const formatterBatchingTestBypass = 1;\n";
204
+
205
+ await expect(formatCode(code, "test-bypass.ts")).resolves.toBe(code);
206
+
207
+ expect(execute).not.toHaveBeenCalled();
208
+ await expect(listTemporaryFiles()).resolves.toEqual([]);
209
+ });
210
+ });
@@ -7,7 +7,7 @@ import path from "node:path";
7
7
  import { afterEach, describe, expect, it, vi } from "vitest";
8
8
  import { z } from "zod";
9
9
 
10
- import { resolveOxlintBin } from "../formatter";
10
+ import { resolveOxlintBin } from "../oxlint";
11
11
 
12
12
  const OxlintManifestSchema = z
13
13
  .union([
@@ -39,7 +39,7 @@ afterEach(async () => {
39
39
 
40
40
  describe("oxlint 실행 파일 해석 회귀", () => {
41
41
  it("설치된 패키지 manifest에서 실행 가능한 로컬 절대 경로를 반환한다", async () => {
42
- const requireFromFormatter = createRequire(new URL("../formatter.ts", import.meta.url));
42
+ const requireFromFormatter = createRequire(new URL("../oxlint.ts", import.meta.url));
43
43
  const manifestPath = requireFromFormatter.resolve("oxlint/package.json");
44
44
  const manifest = OxlintManifestSchema.parse(JSON.parse(await readFile(manifestPath, "utf8")));
45
45
  const expectedBin = path.resolve(path.dirname(manifestPath), manifest.bin);
@@ -0,0 +1,29 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { cached } from "../utils/async-utils";
4
+ import { runOxfmt } from "./oxfmt";
5
+ import { runOxlint } from "./oxlint";
6
+
7
+ /**
8
+ * 코드를 프로젝트의 oxfmt + oxlint 설정에 맞춰 포매팅한 문자열을 반환합니다.
9
+ *
10
+ * 캐싱도 있어요 ㅎㅎ 똑같은 입력에 대해서 캐시 커버됩니다.
11
+ * 수명은 프로세스 죽을때까지 ㅋ
12
+ */
13
+ export const formatCode = cached(formatCodeInternal, (code, filePath) => {
14
+ const ext = filePath.endsWith(".tsx") ? "tsx" : filePath.endsWith(".json") ? "json" : "ts";
15
+ return `${ext}:${createHash("sha1").update(code).digest("hex")}`;
16
+ });
17
+
18
+ /**
19
+ * 캐시 없는 포맷함수 엔트리.
20
+ */
21
+ async function formatCodeInternal(code: string, filePath: string): Promise<string> {
22
+ // json은 포맷만 하면 됩니다.
23
+ if (filePath.endsWith(".json")) {
24
+ return runOxfmt(code, filePath);
25
+ }
26
+
27
+ // 린트 먼저 한 다음에 포맷으로 마무리해요.
28
+ return runOxfmt(await runOxlint(code), filePath);
29
+ }
@@ -0,0 +1,63 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path, { dirname, join } from "node:path";
3
+
4
+ import { format, type FormatConfig } from "oxfmt";
5
+
6
+ import { isTest } from "../utils/controller";
7
+
8
+ /**
9
+ * 프로젝트 설정을 찾아서 이에 맞춰서 코드를 포맷합니다.
10
+ */
11
+ export async function runOxfmt(code: string, filePath: string): Promise<string> {
12
+ const result = await format(path.basename(filePath), code, await loadOxfmtConfig());
13
+
14
+ const errors = result.errors.filter((e) => e.severity === "Error");
15
+ if (errors.length > 0) {
16
+ if (!isTest()) {
17
+ console.error(`oxfmt errors (${filePath}):`);
18
+ for (const err of errors) {
19
+ const label = err.labels[0];
20
+ if (label) {
21
+ const before = code.slice(Math.max(0, label.start - 80), label.start);
22
+ const at = code.slice(label.start, label.end);
23
+ const after = code.slice(label.end, Math.min(code.length, label.end + 80));
24
+ console.error(` - ${err.message} (offset ${label.start}-${label.end})`);
25
+ console.error(` around: ...${before}»${at}«${after}...`);
26
+ } else {
27
+ console.error(` - ${err.message}`);
28
+ }
29
+ }
30
+ }
31
+ return code;
32
+ }
33
+ return result.code;
34
+ }
35
+
36
+ let cachedOxfmtConfig: FormatConfig | null = null;
37
+ async function loadOxfmtConfig(): Promise<FormatConfig> {
38
+ if (cachedOxfmtConfig !== null) {
39
+ return cachedOxfmtConfig;
40
+ }
41
+
42
+ let dir = process.cwd();
43
+ while (true) {
44
+ const candidate = join(dir, ".oxfmtrc.json");
45
+ try {
46
+ // SAFETY: 선행 분기와 함수 계약이 이 타입을 보장합니다.
47
+ cachedOxfmtConfig = JSON.parse(await readFile(candidate, "utf-8")) as FormatConfig;
48
+ return cachedOxfmtConfig;
49
+ } catch (e) {
50
+ // SAFETY: 선행 분기와 함수 계약이 이 타입을 보장합니다.
51
+ if ((e as NodeJS.ErrnoException).code !== "ENOENT") {
52
+ !isTest() && console.error(`Failed to load ${candidate}:`, e);
53
+ break;
54
+ }
55
+ }
56
+ const parent = dirname(dir);
57
+ if (parent === dir) break;
58
+ dir = parent;
59
+ }
60
+
61
+ cachedOxfmtConfig = {};
62
+ return cachedOxfmtConfig;
63
+ }
@@ -1,4 +1,3 @@
1
- import { createHash } from "node:crypto";
2
1
  // 테스트 환경에서는 fs/promises가 mock되지만, 아래 runOxlint이 isTest 가드로 안 도니까
3
2
  // 그냥 fs/promises 그대로 사용. (production에서만 임시파일 흐름이 돕니다.)
4
3
  import { constants } from "node:fs";
@@ -6,13 +5,11 @@ import { access, readFile, unlink, writeFile } from "node:fs/promises";
6
5
  import { createRequire } from "node:module";
7
6
  import path, { dirname, join } from "node:path";
8
7
 
9
- import { format, type FormatConfig } from "oxfmt";
10
8
  import { z } from "zod";
11
9
 
12
- import { cached } from "./async-utils";
13
- import { isTest } from "./controller";
14
- import { execute } from "./process-utils";
15
- import { isNumberValue } from "./runtime-value";
10
+ import { isTest } from "../utils/controller";
11
+ import { execute } from "../utils/process-utils";
12
+ import { isNumberValue } from "../utils/runtime-value";
16
13
 
17
14
  const requireFromHere = createRequire(import.meta.url);
18
15
 
@@ -27,86 +24,16 @@ const OxlintNamedBinManifestSchema = z.object({
27
24
  bin: z.object({ oxlint: z.string().trim().min(1) }),
28
25
  });
29
26
 
30
- /**
31
- * 코드를 프로젝트의 oxfmt + oxlint 설정에 맞춰 포매팅한 문자열을 반환합니다.
32
- *
33
- * 캐싱도 있어요 ㅎㅎ 똑같은 입력에 대해서 캐시 커버됩니다.
34
- * 수명은 프로세스 죽을때까지 ㅋ
35
- */
36
- export const formatCode = cached(formatCodeInternal, (code, filePath) => {
37
- const ext = filePath.endsWith(".tsx") ? "tsx" : filePath.endsWith(".json") ? "json" : "ts";
38
- return `${ext}:${createHash("sha1").update(code).digest("hex")}`;
39
- });
27
+ type OxlintRequest = {
28
+ tmpFile: string;
29
+ resolve: () => void;
30
+ reject: (error: OxlintError) => void;
31
+ };
40
32
 
41
- /**
42
- * 캐시 없는 포맷함수 엔트리.
43
- */
44
- async function formatCodeInternal(code: string, filePath: string): Promise<string> {
45
- // json은 포맷만 하면 됩니다.
46
- if (filePath.endsWith(".json")) {
47
- return runOxfmt(code, filePath);
48
- }
33
+ type OxlintError = Error & { code?: string | number | null };
49
34
 
50
- // 린트 먼저 한 다음에 포맷으로 마무리해요.
51
- return runOxfmt(await runOxlint(code), filePath);
52
- }
53
-
54
- /**
55
- * 프로젝트 설정을 찾아서 이에 맞춰서 코드를 포맷합니다.
56
- */
57
- async function runOxfmt(code: string, filePath: string): Promise<string> {
58
- const result = await format(path.basename(filePath), code, await loadOxfmtConfig());
59
-
60
- const errors = result.errors.filter((e) => e.severity === "Error");
61
- if (errors.length > 0) {
62
- if (!isTest()) {
63
- console.error(`oxfmt errors (${filePath}):`);
64
- for (const err of errors) {
65
- const label = err.labels[0];
66
- if (label) {
67
- const before = code.slice(Math.max(0, label.start - 80), label.start);
68
- const at = code.slice(label.start, label.end);
69
- const after = code.slice(label.end, Math.min(code.length, label.end + 80));
70
- console.error(` - ${err.message} (offset ${label.start}-${label.end})`);
71
- console.error(` around: ...${before}»${at}«${after}...`);
72
- } else {
73
- console.error(` - ${err.message}`);
74
- }
75
- }
76
- }
77
- return code;
78
- }
79
- return result.code;
80
- }
81
-
82
- let cachedOxfmtConfig: FormatConfig | null = null;
83
- async function loadOxfmtConfig(): Promise<FormatConfig> {
84
- if (cachedOxfmtConfig !== null) {
85
- return cachedOxfmtConfig;
86
- }
87
-
88
- let dir = process.cwd();
89
- while (true) {
90
- const candidate = join(dir, ".oxfmtrc.json");
91
- try {
92
- // SAFETY: 선행 분기와 함수 계약이 이 타입을 보장합니다.
93
- cachedOxfmtConfig = JSON.parse(await readFile(candidate, "utf-8")) as FormatConfig;
94
- return cachedOxfmtConfig;
95
- } catch (e) {
96
- // SAFETY: 선행 분기와 함수 계약이 이 타입을 보장합니다.
97
- if ((e as NodeJS.ErrnoException).code !== "ENOENT") {
98
- !isTest() && console.error(`Failed to load ${candidate}:`, e);
99
- break;
100
- }
101
- }
102
- const parent = dirname(dir);
103
- if (parent === dir) break;
104
- dir = parent;
105
- }
106
-
107
- cachedOxfmtConfig = {};
108
- return cachedOxfmtConfig;
109
- }
35
+ const oxlintQueue: OxlintRequest[] = [];
36
+ let isOxlintRunning = false;
110
37
 
111
38
  /**
112
39
  * 프로젝트 설정에 맞춰 코드를 lint합니다.
@@ -114,8 +41,23 @@ async function loadOxfmtConfig(): Promise<FormatConfig> {
114
41
  * 프로젝트 설정을 적용받는 oxlint cli를 찾아 띄워서,
115
42
  * 임시 파일에 in-place로 써서 그 결과를 빼오는 방식으로 작동합니다.
116
43
  * 왜 이렇게 하느냐? oxlint가 node api도 안 주고 cli에서 stdin 옵션도 안 주기 때문...
44
+ *
45
+ * 그런데 이 친구는 호출할 때마다 무지성으로 oxlint cli를 돌리지는 않습니다.
46
+ * 그런 식으로 했다가는 oxlint 프로세스만 동시에 수십 개가 떠 있게 될 수도 있어요.
47
+ * 그러다가 sync에서 파일 생성이 10초도 넘게 걸리는 일도 있었습니다.
48
+ * 이를 막기 위해, oxlint가 돌아가고 있을 때에 새 runOxlint 호출이 들어오면 내부에서 알아서 잘 쟁여놨다가 실행 끝나면 배치로 굴립니다.
49
+ * 한 번에 오직 하나의 oxlint cli 실행만 돌아가게 하는 겁니다.
50
+ *
51
+ * 자세한 배칭 전략:
52
+ * - 아무 것도 없을 때에 호출되면 바로 oxlint 실행합니다.
53
+ * - oxlint 실행 중에 들어오면 큐에 쟁여둡니다.
54
+ * - oxlint 실행 완료 시점에서 큐에 뭐가 있으면 그걸로 다음 배치 바로 실행합니다.
55
+ *
56
+ * 그래서 띄엄띄엄 하나씩 들어오면 그냥 순차 실행과 다름이 없고, 여러개 우루루 들어오면 그때 효용이 생깁니다.
57
+ * 다만 동시에 30개가 한번에 들어온다고 30개가 모두 한 배치를 타지는 않고,
58
+ * idle에서의 첫 요청에 즉시 배치를 시작하므로 최소 2배치가 소요된다는 점 주의염.
117
59
  */
118
- async function runOxlint(code: string): Promise<string> {
60
+ export async function runOxlint(code: string): Promise<string> {
119
61
  if (isTest()) {
120
62
  // 테스트 환경에서는 느려지기만 하고 검증할 가치도 없어서 안 합니다.
121
63
  // GitHub Actions 환경에서 lint가 오래 걸려서 뻗기도 했어요. (https://github.com/cartanova-ai/sonamu/actions/runs/25267214027/job/74083630169)
@@ -125,34 +67,64 @@ async function runOxlint(code: string): Promise<string> {
125
67
  const tmpFile = join(
126
68
  // 타겟 파일이 루트 아래에 있어야 해요. 그래서 tmp 디렉토리같은거 안 씁니다!
127
69
  process.cwd(),
128
- `.sonamu-fmt-${Date.now()}-${Math.random().toString(36).slice(2)}.ts`,
70
+ `.sonamu-oxlint-${Date.now()}-${Math.random().toString(36).slice(2)}.ts`,
129
71
  );
130
72
 
131
73
  try {
132
74
  await writeFile(tmpFile, code, "utf-8");
75
+ await enqueueOxlint(tmpFile);
76
+ return await readFile(tmpFile, "utf-8");
77
+ } finally {
78
+ try {
79
+ await unlink(tmpFile);
80
+ } catch {
81
+ // 삭제 실패해도 어차피 ignore됨.
82
+ }
83
+ }
84
+ }
85
+
86
+ function enqueueOxlint(tmpFile: string): Promise<void> {
87
+ return new Promise((resolve, reject) => {
88
+ oxlintQueue.push({ tmpFile, resolve, reject });
89
+ if (!isOxlintRunning) {
90
+ void flushOxlintQueue();
91
+ }
92
+ });
93
+ }
94
+
95
+ async function flushOxlintQueue(): Promise<void> {
96
+ if (isOxlintRunning || oxlintQueue.length === 0) {
97
+ return;
98
+ }
99
+
100
+ isOxlintRunning = true;
101
+ // 실행을 시작할 때까지 준비된 파일만 묶어 다음 요청은 후속 배치로 넘깁니다.
102
+ const batch = oxlintQueue.splice(0);
133
103
 
104
+ try {
134
105
  try {
135
106
  await execute(
136
107
  await resolveOxlintBin(),
137
- ["--fix", "--fix-suggestions", "--type-aware", tmpFile],
108
+ ["--fix", "--fix-suggestions", "--type-aware", ...batch.map(({ tmpFile }) => tmpFile)],
138
109
  {
139
110
  timeout: 10000,
140
111
  },
141
112
  );
142
113
  } catch (e) {
143
- // lint 위반 시 exit code != 0이지만 --fix는 적용됨. exec 자체 실패만 throw.
114
+ // lint 위반 시 exit code != 0이지만 --fix는 적용됨. exec 자체 실패만 배치 전체에 전달합니다.
144
115
  // SAFETY: 선행 분기와 함수 계약이 이 타입을 보장합니다.
145
- if (!isNumberValue((e as Error & { code?: number }).code)) {
146
- throw e;
116
+ const error = e as OxlintError;
117
+ if (!isNumberValue(error.code)) {
118
+ batch.forEach(({ reject }) => reject(error));
119
+ return;
147
120
  }
148
121
  }
149
122
 
150
- return await readFile(tmpFile, "utf-8");
123
+ batch.forEach(({ resolve }) => resolve());
151
124
  } finally {
152
- try {
153
- await unlink(tmpFile);
154
- } catch {
155
- // 삭제 실패해도 어차피 ignore됨.
125
+ isOxlintRunning = false;
126
+ if (oxlintQueue.length > 0) {
127
+ void flushOxlintQueue();
156
128
  }
157
129
  }
158
130
  }
@@ -6,9 +6,9 @@ import { Workbook } from "@sheetkit/node";
6
6
  import ts from "@typescript/typescript6";
7
7
 
8
8
  import { Sonamu } from "../api/sonamu";
9
+ import { formatCode } from "../code-formatting";
9
10
  import { EntityManager } from "../entity/entity-manager";
10
11
  import { BadRequestException } from "../exceptions/so-exceptions";
11
- import { formatCode } from "../utils/formatter";
12
12
  import { SD } from "./sd";
13
13
  import {
14
14
  type DictEntry,
@@ -7,6 +7,7 @@ import { group, unique } from "radashi";
7
7
  import { z } from "zod";
8
8
 
9
9
  import { Sonamu } from "../api/sonamu";
10
+ import { formatCode } from "../code-formatting";
10
11
  import {
11
12
  getEnumDefValues,
12
13
  getSubsetFields,
@@ -33,7 +34,6 @@ import {
33
34
  type SubsetQuery,
34
35
  } from "../types/types";
35
36
  import { importMembers } from "../utils/esm-utils";
36
- import { formatCode } from "../utils/formatter";
37
37
  import { exists } from "../utils/fs-utils";
38
38
  import { runtimePath } from "../utils/path-utils";
39
39
  import { isStringValue } from "../utils/runtime-value";
@@ -2,6 +2,7 @@ import equal from "fast-deep-equal";
2
2
  import { type Knex } from "knex";
3
3
  import { alphabetical, diff } from "radashi";
4
4
 
5
+ import { formatCode } from "../code-formatting";
5
6
  import { EntityManager } from "../entity/entity-manager";
6
7
  import { Naite } from "../naite/naite";
7
8
  import {
@@ -13,7 +14,6 @@ import {
13
14
  type MigrationSet,
14
15
  } from "../types/types";
15
16
  import { isSearchTextProp } from "../types/types";
16
- import { formatCode } from "../utils/formatter";
17
17
  import { isStringValue } from "../utils/runtime-value";
18
18
  import { differenceWith, intersectionBy } from "../utils/utils";
19
19
  import {