create-nexwire 4.2.5 → 4.3.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.
Files changed (85) hide show
  1. package/README.md +20 -8
  2. package/express/.env +1 -1
  3. package/express/package.json +1 -1
  4. package/express/src/config/database.ts +48 -29
  5. package/express/src/config/index.ts +83 -83
  6. package/express/src/config/openapi.ts +6 -24
  7. package/express/src/config/queue.ts +30 -30
  8. package/express/src/config/validate.ts +50 -40
  9. package/express/src/database/migrations/sqlite/0000_init.sql +47 -47
  10. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  11. package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
  12. package/express/src/framework/cache/cache.ts +40 -8
  13. package/express/src/framework/circuit-breaker/cb.ts +88 -60
  14. package/express/src/framework/database/connection.ts +52 -27
  15. package/express/src/framework/events/dispatcher.ts +61 -49
  16. package/express/src/framework/facade.ts +46 -3
  17. package/express/src/framework/http/app.ts +28 -22
  18. package/express/src/framework/http/logger.ts +27 -16
  19. package/express/src/framework/http/openapi.ts +9 -11
  20. package/express/src/framework/modules/routes.ts +26 -21
  21. package/express/src/framework/notification/channels/broadcast.ts +15 -0
  22. package/express/src/framework/notification/channels/mail.ts +40 -0
  23. package/express/src/framework/notification/contracts.ts +29 -0
  24. package/express/src/framework/notification/index.ts +19 -94
  25. package/express/src/framework/notification/registry.ts +15 -0
  26. package/express/src/framework/providers/adapter.ts +27 -17
  27. package/express/src/framework/queue/queue.ts +43 -44
  28. package/express/src/framework/realtime/broadcast.ts +29 -5
  29. package/express/src/framework/realtime/socket.ts +149 -119
  30. package/express/src/framework/redis/client.ts +148 -151
  31. package/express/src/framework/runtime/server-adapter.ts +39 -34
  32. package/express/src/framework/scheduler/lock.ts +128 -116
  33. package/express/src/framework/server.ts +33 -49
  34. package/express/src/framework/session/session.ts +69 -14
  35. package/express/src/framework/storage/storage.ts +744 -785
  36. package/express/src/framework/support/mail.ts +15 -7
  37. package/express/src/providers/notification.ts +4 -0
  38. package/express/src/resources/src/layouts/Layout/Header.vue +124 -105
  39. package/express/src/resources/src/plugins/pulse.ts +157 -147
  40. package/express/src/resources/vite.config.ts +54 -43
  41. package/express/src/storage/database/nexwire.sqlite +0 -0
  42. package/express/src/storage/logs/app.log +0 -0
  43. package/express/src/storage/logs/fatal.log +0 -0
  44. package/express/src/types/global.d.ts +32 -26
  45. package/hono/.env +1 -1
  46. package/hono/package.json +1 -1
  47. package/hono/src/config/database.ts +48 -29
  48. package/hono/src/config/index.ts +83 -83
  49. package/hono/src/config/openapi.ts +6 -24
  50. package/hono/src/config/queue.ts +30 -30
  51. package/hono/src/config/validate.ts +50 -40
  52. package/hono/src/database/migrations/sqlite/0000_init.sql +47 -47
  53. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  54. package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
  55. package/hono/src/framework/cache/cache.ts +40 -8
  56. package/hono/src/framework/circuit-breaker/cb.ts +88 -60
  57. package/hono/src/framework/database/connection.ts +52 -27
  58. package/hono/src/framework/events/dispatcher.ts +61 -49
  59. package/hono/src/framework/facade.ts +38 -1
  60. package/hono/src/framework/http/openapi.ts +7 -9
  61. package/hono/src/framework/notification/channels/broadcast.ts +15 -0
  62. package/hono/src/framework/notification/channels/mail.ts +40 -0
  63. package/hono/src/framework/notification/contracts.ts +29 -0
  64. package/hono/src/framework/notification/index.ts +19 -94
  65. package/hono/src/framework/notification/registry.ts +15 -0
  66. package/hono/src/framework/providers/adapter.ts +27 -17
  67. package/hono/src/framework/queue/queue.ts +43 -44
  68. package/hono/src/framework/realtime/broadcast.ts +29 -5
  69. package/hono/src/framework/realtime/socket.ts +217 -196
  70. package/hono/src/framework/redis/client.ts +148 -151
  71. package/hono/src/framework/scheduler/lock.ts +128 -116
  72. package/hono/src/framework/server.ts +27 -29
  73. package/hono/src/framework/session/session.ts +62 -14
  74. package/hono/src/framework/storage/storage.ts +744 -785
  75. package/hono/src/framework/support/mail.ts +56 -48
  76. package/hono/src/providers/notification.ts +4 -0
  77. package/hono/src/resources/src/layouts/Layout/Header.vue +124 -105
  78. package/hono/src/resources/src/plugins/pulse.ts +157 -147
  79. package/hono/src/resources/vite.config.ts +54 -43
  80. package/hono/src/storage/database/nexwire.sqlite +0 -0
  81. package/hono/src/storage/logs/app.log +0 -0
  82. package/hono/src/storage/logs/fatal.log +0 -0
  83. package/hono/src/types/global.d.ts +32 -26
  84. package/package.json +2 -2
  85. package/src/index.js +139 -14
@@ -1,53 +1,64 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath, URL } from "node:url";
3
3
  import vue from "@vitejs/plugin-vue";
4
- import { defineConfig } from "vite";
4
+ import { defineConfig, loadEnv } from "vite";
5
5
 
6
- const cacheBase = process.env.LOCALAPPDATA || process.env.TEMP || ".";
7
- const apiUrl = process.env.APP_URL || "http://localhost:3000";
8
- const socketEnabled = process.env.SOCKET !== "false";
6
+ const configDir = path.dirname(fileURLToPath(import.meta.url));
7
+ const projectRoot = path.resolve(configDir, "../..");
9
8
 
10
- const proxy: Record<string, any> = {
11
- "/api": { target: apiUrl, changeOrigin: true },
12
- "/health": { target: apiUrl, changeOrigin: true },
13
- "/storage": { target: apiUrl, changeOrigin: true }
14
- };
9
+ export default defineConfig(({ mode }) => {
10
+ // 1. Load the .env file just like the backend does. Resolve it from the
11
+ // config file so a different process cwd cannot point Vite at the wrong .env.
12
+ const env = loadEnv(mode, projectRoot, "");
15
13
 
16
- proxy["/socket.io"] = { target: apiUrl, changeOrigin: true, ws: true };
14
+ const cacheBase = process.env.LOCALAPPDATA || process.env.TEMP || ".";
15
+ const apiUrl = env.APP_URL || "http://localhost:3000";
17
16
 
18
- export default defineConfig({
19
- define: {
20
- __SOCKET_ENABLED__: socketEnabled,
21
- __API_URL__: JSON.stringify(apiUrl)
22
- },
23
- root: fileURLToPath(new URL(".", import.meta.url)),
24
- cacheDir: path.join(cacheBase, "nexwire", "vite-cache", "resources"),
25
- plugins: [vue()],
26
- css: {
27
- preprocessorOptions: {
28
- scss: {
29
- quietDeps: true,
30
- silenceDeprecations: [
31
- "import",
32
- "if-function",
33
- "global-builtin",
34
- "color-functions",
35
- "slash-div",
36
- ],
17
+ // Match the backend default: Socket.IO is off unless SOCKET=true
18
+ const socketEnabled = env.SOCKET === "true";
19
+
20
+ const proxy: Record<string, any> = {
21
+ "/api": { target: apiUrl, changeOrigin: true },
22
+ "/health": { target: apiUrl, changeOrigin: true },
23
+ "/storage": { target: apiUrl, changeOrigin: true }
24
+ };
25
+
26
+ if (socketEnabled) {
27
+ proxy["/socket.io"] = { target: apiUrl, changeOrigin: true, ws: true, timeout: 10000 };
28
+ }
29
+
30
+ return {
31
+ define: {
32
+ // 3. ALWAYS stringify the value when using Vite define
33
+ // `import.meta.env.*` is inlined into every module in dev as well as in
34
+ // the production bundle, so the UI never depends on a stale global.
35
+ "import.meta.env.VITE_SOCKET_ENABLED": JSON.stringify(socketEnabled),
36
+ __SOCKET_ENABLED__: JSON.stringify(socketEnabled),
37
+ __API_URL__: JSON.stringify(apiUrl)
38
+ },
39
+ root: fileURLToPath(new URL(".", import.meta.url)),
40
+ cacheDir: path.join(cacheBase, "nexwire", "vite-cache", "resources"),
41
+ plugins: [vue()],
42
+ css: {
43
+ preprocessorOptions: {
44
+ scss: {
45
+ quietDeps: true,
46
+ silenceDeprecations: ["import", "if-function", "global-builtin", "color-functions", "slash-div"]
47
+ }
37
48
  }
49
+ },
50
+ resolve: {
51
+ alias: {
52
+ "@": fileURLToPath(new URL("./src", import.meta.url))
53
+ }
54
+ },
55
+ build: {
56
+ outDir: "../../public",
57
+ emptyOutDir: true
58
+ },
59
+ server: {
60
+ port: 5173,
61
+ proxy // /socket.io is only proxied when SOCKET=true
38
62
  }
39
- },
40
- resolve: {
41
- alias: {
42
- "@": fileURLToPath(new URL("./src", import.meta.url))
43
- }
44
- },
45
- build: {
46
- outDir: "../../public",
47
- emptyOutDir: true
48
- },
49
- server: {
50
- port: 5173,
51
- proxy
52
- }
63
+ };
53
64
  });
File without changes
File without changes
@@ -1,26 +1,32 @@
1
- declare module "*.vue" {
2
- import type { DefineComponent } from "vue";
3
-
4
- const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
5
- export default component;
6
- }
7
-
8
- declare module "bootstrap/js/dist/modal.js" {
9
- export default class Modal {
10
- constructor(element: Element, options?: { backdrop?: boolean | "static"; keyboard?: boolean });
11
- show(): void;
12
- hide(): void;
13
- dispose(): void;
14
- }
15
- }
16
-
17
- declare module "bootstrap/js/dist/toast.js" {
18
- export default class Toast {
19
- static getOrCreateInstance(element: Element): Toast;
20
- show(): void;
21
- }
22
- }
23
-
24
- declare module "luxon";
25
- declare module "nodemailer";
26
- declare module "vue-select";
1
+ declare module "*.vue" {
2
+ import type { DefineComponent } from "vue";
3
+
4
+ const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
5
+ export default component;
6
+ }
7
+
8
+ declare module "bootstrap/js/dist/modal.js" {
9
+ export default class Modal {
10
+ constructor(element: Element, options?: { backdrop?: boolean | "static"; keyboard?: boolean });
11
+ show(): void;
12
+ hide(): void;
13
+ dispose(): void;
14
+ }
15
+ }
16
+
17
+ declare module "bootstrap/js/dist/toast.js" {
18
+ export default class Toast {
19
+ static getOrCreateInstance(element: Element): Toast;
20
+ show(): void;
21
+ }
22
+ }
23
+
24
+ declare module "luxon";
25
+ declare module "nodemailer";
26
+ declare module "vue-select";
27
+
28
+ interface ImportMetaEnv {
29
+ readonly VITE_API_URL?: string;
30
+ readonly VITE_SOCKET_URL?: string;
31
+ readonly VITE_SOCKET_ENABLED?: boolean;
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nexwire",
3
- "version": "4.2.5",
3
+ "version": "4.3.0",
4
4
  "type": "module",
5
5
  "description": "Scaffold a new nexwire project",
6
6
  "bin": {
@@ -72,7 +72,7 @@
72
72
  "ioredis": "^5",
73
73
  "lodash-es": "^4",
74
74
  "luxon": "^3",
75
- "maildev": "^3",
75
+ "maildev": "^2.2.1",
76
76
  "mysql2": "^3",
77
77
  "nodemailer": "^10",
78
78
  "pinia": "^3",
package/src/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { readFileSync, existsSync, cpSync, mkdirSync, writeFileSync, renameSync, readdirSync, rmSync } from "node:fs";
4
4
  import { join, dirname, basename } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { createInterface } from "node:readline";
6
+ import { createInterface, emitKeypressEvents } from "node:readline";
7
7
  import { gunzipSync } from "node:zlib";
8
8
  import https from "node:https";
9
9
 
@@ -18,6 +18,104 @@ function ask(query) {
18
18
  return new Promise((resolve) => rl.question(query, (a) => { rl.close(); resolve(a.trim()); }));
19
19
  }
20
20
 
21
+ // Keep this in sync with the packages/ directories shipped in the tarball and
22
+ // with ENGINE_PREFIXES below. A future frontend adds its own list here and its
23
+ // own prefix inside extractTarGz.
24
+ const BACKENDS = [
25
+ { value: "hono", label: "Hono", hint: "Hono-based HTTP layer and OpenAPI" },
26
+ { value: "express", label: "Express", hint: "Express-based HTTP layer and OpenAPI" }
27
+ ];
28
+
29
+ const BACKEND_NAMES = BACKENDS.map((b) => b.value);
30
+
31
+ function backendIndex(value) {
32
+ return BACKENDS.findIndex((b) => b.value === String(value).toLowerCase());
33
+ }
34
+
35
+ const useColor = Boolean(process.stdout.isTTY && !process.env.NO_COLOR);
36
+ const paint = (code, text) => (useColor ? `\x1b[${code}m${text}\x1b[0m` : text);
37
+
38
+ /**
39
+ * Render an arrow-key list picker. Falls back to the default choice when
40
+ * stdin is not a TTY (CI, piped input) so `npm create nexwire@latest .`
41
+ * keeps working in scripts without hanging on a prompt.
42
+ */
43
+ async function selectOption(title, options, defaultIndex = 0) {
44
+ if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== "function") {
45
+ return options[defaultIndex].value;
46
+ }
47
+
48
+ const stdin = process.stdin;
49
+ const out = process.stdout;
50
+ let index = defaultIndex;
51
+ const lines = options.length + 2; // title + options + trailing help line
52
+
53
+ emitKeypressEvents(stdin);
54
+ stdin.setRawMode(true);
55
+ stdin.resume();
56
+ out.write("\x1b[?25l"); // hide the cursor while the list is live
57
+
58
+ const render = (first) => {
59
+ if (!first) out.write(`\x1b[${lines}A`);
60
+ out.write(`${paint(1, title)}\n`);
61
+ options.forEach((option, i) => {
62
+ const active = i === index;
63
+ const marker = active ? paint(32, "\u276F") : " ";
64
+ const label = paint(active ? 1 : 0, option.label);
65
+ const hint = option.hint ? ` ${paint(2, option.hint)}` : "";
66
+ out.write(`${marker} ${label}${hint}\x1b[K\n`);
67
+ });
68
+ out.write(`${paint(2, "Arrow keys choose, 1-2 jump, Enter confirm")}\n`);
69
+ };
70
+
71
+ render(true);
72
+
73
+ return new Promise((resolve) => {
74
+ const cleanup = () => {
75
+ stdin.setRawMode(false);
76
+ stdin.removeListener("keypress", onKeypress);
77
+ stdin.pause();
78
+ out.write("\x1b[?25h"); // restore the cursor
79
+ };
80
+
81
+ const onKeypress = (str, key = {}) => {
82
+ if (key.ctrl && key.name === "c") {
83
+ cleanup();
84
+ out.write("\n");
85
+ process.exit(130);
86
+ }
87
+ if (key.name === "escape") {
88
+ cleanup();
89
+ out.write("\n");
90
+ process.exit(130);
91
+ }
92
+ if (key.name === "up" || key.name === "k") {
93
+ index = (index - 1 + options.length) % options.length;
94
+ render(false);
95
+ return;
96
+ }
97
+ if (key.name === "down" || key.name === "j") {
98
+ index = (index + 1) % options.length;
99
+ render(false);
100
+ return;
101
+ }
102
+ if (key.name === "return" || key.name === "enter") {
103
+ cleanup();
104
+ out.write("\n");
105
+ resolve(options[index].value);
106
+ return;
107
+ }
108
+ const digit = Number(str);
109
+ if (Number.isInteger(digit) && digit >= 1 && digit <= options.length) {
110
+ index = digit - 1;
111
+ render(false);
112
+ }
113
+ };
114
+
115
+ stdin.on("keypress", onKeypress);
116
+ });
117
+ }
118
+
21
119
  function detectPackageManager() {
22
120
  if (typeof process !== "undefined" && process.isBun) return "bun";
23
121
  const ua = process.env.npm_config_user_agent || "";
@@ -116,26 +214,26 @@ function fetchTarball(pkgName, pkgVersion) {
116
214
 
117
215
  async function main() {
118
216
  const args = process.argv.slice(2);
119
- let projectName = args.find((a) => !a.startsWith("--"));
120
-
217
+ const yes = args.includes("--yes") || args.includes("-y");
121
218
  const engineArg = args.find((a) => a.startsWith("--engine="));
122
- let engine = engineArg ? engineArg.split("=")[1] : "hono";
123
-
124
- if (!ENGINE_PREFIXES[engine]) {
125
- console.error(`Error: Unknown engine "${engine}". Supported engines: hono, express.`);
126
- process.exit(1);
127
- }
219
+ let projectName = args.find((a) => !a.startsWith("--"));
128
220
 
221
+ // --help works even with --engine set, and before any prompt or network call.
129
222
  if (args.includes("--help") || args.includes("-h")) {
130
223
  console.log(`create-nexwire v${version}`);
131
224
  console.log();
132
225
  console.log("Usage:");
133
- console.log(" npm create nexwire@latest <project-name|.> [--engine=<hono|express>]");
134
- console.log(" pnpm create nexwire@latest <project-name|.> [--engine=<hono|express>]");
135
- console.log(" bun create nexwire@latest <project-name|.> [--engine=<hono|express>]");
136
- console.log(" npx create-nexwire@latest <project-name|.> [--engine=<hono|express>]");
226
+ console.log(" npm create nexwire@latest <project-name|.> [--engine=<hono|express>] [--yes]");
227
+ console.log(" pnpm create nexwire@latest <project-name|.> [--engine=<hono|express>] [--yes]");
228
+ console.log(" bun create nexwire@latest <project-name|.> [--engine=<hono|express>] [--yes]");
229
+ console.log(" npx create-nexwire@latest <project-name|.> [--engine=<hono|express>] [--yes]");
137
230
  console.log();
138
- console.log("Engines:");
231
+ console.log("Options:");
232
+ console.log(" --engine=<hono|express> Backend to scaffold. Asked interactively when omitted.");
233
+ console.log(" -y, --yes Skip prompts and accept the defaults (requires a project name).");
234
+ console.log(" -h, --help Show this help.");
235
+ console.log();
236
+ console.log("Backends:");
139
237
  console.log(" hono (default) Hono-based HTTP layer and OpenAPI");
140
238
  console.log(" express Express-based HTTP layer and OpenAPI");
141
239
  console.log();
@@ -144,7 +242,25 @@ async function main() {
144
242
  process.exit(0);
145
243
  }
146
244
 
245
+ if (engineArg === undefined && args.includes("--engine")) {
246
+ console.error('Error: Missing value for --engine. Use --engine=hono or --engine=express.');
247
+ process.exit(1);
248
+ }
249
+
250
+ if (engineArg) {
251
+ const engine = engineArg.split("=")[1].toLowerCase();
252
+ if (backendIndex(engine) === -1) {
253
+ console.error(`Error: Unknown engine "${engine}". Supported backends: ${BACKEND_NAMES.join(", ")}.`);
254
+ process.exit(1);
255
+ }
256
+ }
257
+
147
258
  if (!projectName) {
259
+ if (!process.stdin.isTTY || yes) {
260
+ console.error("Error: Please provide a project name or '.' to scaffold into the current directory.");
261
+ console.error(" A project name is required when the CLI runs without an interactive terminal.");
262
+ process.exit(1);
263
+ }
148
264
  projectName = await ask("Project name or '.' for the current directory: ");
149
265
  }
150
266
 
@@ -158,6 +274,15 @@ async function main() {
158
274
  process.exit(1);
159
275
  }
160
276
 
277
+ let engine = engineArg ? engineArg.split("=")[1].toLowerCase() : null;
278
+ if (!engine) {
279
+ if (yes) {
280
+ engine = "hono";
281
+ } else {
282
+ engine = await selectOption("Choose a backend:", BACKENDS, 0);
283
+ }
284
+ }
285
+
161
286
  const targetDir = resolveTargetDir(projectName);
162
287
  const isCurrentDir = targetDir === process.cwd();
163
288