create-nexwire 4.2.1 → 4.2.2

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.
@@ -13,8 +13,16 @@ import { PROJECT_COMMAND_FLAG } from "./help.mjs";
13
13
  * there would be overwritten on the next framework update - and `src/modules/`
14
14
  * is swept by the route, job and seeder globs. The project root is the one
15
15
  * place nothing else owns.
16
+ *
17
+ * Two names are read: commands.mjs (plain JavaScript, loaded by plain node)
18
+ * and commands.ts (TypeScript, loaded through tsx). The .mjs is the incumbent
19
+ * and wins if a project keeps both.
16
20
  */
17
21
  export const PROJECT_COMMANDS_PATH = "maker/commands.mjs";
22
+ export const PROJECT_COMMANDS_TS_PATH = "maker/commands.ts";
23
+
24
+ /** Process-global so a re-evaluated module cannot register tsx twice. */
25
+ const TSX_REGISTERED = "__nexwireMakerTsxRegistered";
18
26
 
19
27
  const PROJECT_COMMANDS_TEMPLATE = `/**
20
28
  * Your project's maker commands.
@@ -36,8 +44,11 @@ const PROJECT_COMMANDS_TEMPLATE = `/**
36
44
  * which one. Rename yours; the built-in stays.
37
45
  * - An error in this file is reported, never swallowed. If your commands stop
38
46
  * appearing, the fault is here rather than in your command code.
39
- * - Import your own code with a normal relative path, e.g.
40
- * import { sendInvoice } from "../src/invoices/send.js";
47
+ * - This file runs as plain JavaScript under plain Node, so it cannot import
48
+ * your TypeScript source or use your tsconfig path alias. If a command
49
+ * needs either, name this file maker/commands.ts instead and delete this
50
+ * one - nexwire loads that file through tsx, with your tsconfig applied.
51
+ * For editor type support, create maker/tsconfig.json as shown in the docs.
41
52
  *
42
53
  * Replace or delete the example below once you have a command of your own.
43
54
  */
@@ -54,14 +65,47 @@ export default function register(program) {
54
65
  }
55
66
  `;
56
67
 
68
+ /**
69
+ * Why: Plain node cannot import TypeScript, so maker/commands.ts needs tsx
70
+ * registered before it is imported. Registered at most once per process
71
+ * and never unregistered: two overlapping registrations corrupt the
72
+ * loader, and commands routinely import more TypeScript at action time,
73
+ * long after this function returns. The flag lives on globalThis because
74
+ * this module can be evaluated more than once in one process (vitest's
75
+ * per-file isolation) while the loader hooks are process-wide.
76
+ * When: Only when the project's file is TypeScript - maker/commands.mjs never
77
+ * pays for tsx being installed.
78
+ * Where: utils/project-commands.mjs.
79
+ */
80
+ async function registerTsLoader(rel) {
81
+ if (globalThis[TSX_REGISTERED]) return;
82
+
83
+ try {
84
+ const { register } = await import("tsx/esm/api");
85
+ register();
86
+ } catch (error) {
87
+ throw new Error(
88
+ `${rel} needs tsx to load TypeScript, but it could not be registered: ${error?.message || error}. tsx ships as a devDependency of every nexwire project - run npm install, or rename the file to ${PROJECT_COMMANDS_PATH}.`,
89
+ { cause: error }
90
+ );
91
+ }
92
+
93
+ globalThis[TSX_REGISTERED] = true;
94
+ }
95
+
57
96
  /**
58
97
  * Why: Registers the commands a project defined for itself.
59
98
  * When: Every maker invocation, right after the framework's own registrars.
60
99
  * Where: Maker CLI entry flow, from utils/project-commands.mjs.
61
- * How: Imports maker/commands.mjs resolved from process.cwd() - not from
62
- * import.meta.dirname, which points inside node_modules and would find the
63
- * framework's own directory instead of the project. Absent is silent and
64
- * free; broken is loud, because a developer's commands silently not
100
+ * How: Imports maker/commands.mjs - or maker/commands.ts, loaded through tsx,
101
+ * which resolves the project's tsconfig path aliases - resolved from
102
+ * process.cwd(), not from import.meta.dirname, which points inside
103
+ * node_modules and would find the framework's own directory instead of the
104
+ * project. If a project keeps both files the .mjs wins and the .ts file is
105
+ * skipped with a warning naming it, so neither file is ever silently dead:
106
+ * a project that already runs must not be re-registered by a file that
107
+ * appeared later, and a file that is skipped must say so. Absent is silent
108
+ * and free; broken is loud, because a developer's commands silently not
65
109
  * existing is a far worse experience than an error message. A name that
66
110
  * collides with a framework command is refused by commander itself, which
67
111
  * is the right outcome: the built-in wins and the project cannot shadow a
@@ -70,10 +114,23 @@ export default function register(program) {
70
114
  * asked for the name.
71
115
  */
72
116
  export async function registerProjectCommands(program, rawArgs, cwd = process.cwd()) {
73
- const file = path.resolve(cwd, PROJECT_COMMANDS_PATH);
117
+ const mjsFile = path.resolve(cwd, PROJECT_COMMANDS_PATH);
118
+ const tsFile = path.resolve(cwd, PROJECT_COMMANDS_TS_PATH);
119
+ const hasMjs = fs.existsSync(mjsFile);
120
+ const hasTs = fs.existsSync(tsFile);
121
+
122
+ if (!hasMjs && !hasTs) {
123
+ return { loaded: false, file: mjsFile, added: [] };
124
+ }
125
+
126
+ const isTs = !hasMjs && hasTs;
127
+ const file = isTs ? tsFile : mjsFile;
128
+ const rel = isTs ? PROJECT_COMMANDS_TS_PATH : PROJECT_COMMANDS_PATH;
74
129
 
75
- if (!fs.existsSync(file)) {
76
- return { loaded: false, file, added: [] };
130
+ if (hasMjs && hasTs) {
131
+ console.warn(
132
+ `${PROJECT_COMMANDS_TS_PATH} is ignored because ${PROJECT_COMMANDS_PATH} exists. Keep one - delete the file you are not using.`
133
+ );
77
134
  }
78
135
 
79
136
  // Not wrapped in a bare try/catch on purpose. A file that exists but fails to
@@ -81,18 +138,22 @@ export async function registerProjectCommands(program, rawArgs, cwd = process.cw
81
138
  // command that is actually in this loader. The catches below only attach the
82
139
  // file name, because an error from inside the project file otherwise reads as
83
140
  // if it came from the framework.
141
+ if (isTs) {
142
+ await registerTsLoader(rel);
143
+ }
144
+
84
145
  let module;
85
146
  try {
86
147
  module = await import(pathToFileURL(file).href);
87
148
  } catch (error) {
88
- throw new Error(`Could not load ${PROJECT_COMMANDS_PATH}: ${error?.message || error}`, { cause: error });
149
+ throw new Error(`Could not load ${rel}: ${error?.message || error}`, { cause: error });
89
150
  }
90
151
 
91
152
  const register = module.default ?? module.register;
92
153
 
93
154
  if (typeof register !== "function") {
94
155
  throw new TypeError(
95
- `${PROJECT_COMMANDS_PATH} must export a default function (program, args) => void. Found: ${typeof register}.`
156
+ `${rel} must export a default function (program, args) => void. Found: ${typeof register}.`
96
157
  );
97
158
  }
98
159
 
@@ -101,7 +162,7 @@ export async function registerProjectCommands(program, rawArgs, cwd = process.cw
101
162
  try {
102
163
  await register(program, rawArgs);
103
164
  } catch (error) {
104
- throw new Error(`Could not register commands from ${PROJECT_COMMANDS_PATH}: ${error?.message || error}`, {
165
+ throw new Error(`Could not register commands from ${rel}: ${error?.message || error}`, {
105
166
  cause: error
106
167
  });
107
168
  }
@@ -13,8 +13,16 @@ import { PROJECT_COMMAND_FLAG } from "./help.mjs";
13
13
  * there would be overwritten on the next framework update - and `src/modules/`
14
14
  * is swept by the route, job and seeder globs. The project root is the one
15
15
  * place nothing else owns.
16
+ *
17
+ * Two names are read: commands.mjs (plain JavaScript, loaded by plain node)
18
+ * and commands.ts (TypeScript, loaded through tsx). The .mjs is the incumbent
19
+ * and wins if a project keeps both.
16
20
  */
17
21
  export const PROJECT_COMMANDS_PATH = "maker/commands.mjs";
22
+ export const PROJECT_COMMANDS_TS_PATH = "maker/commands.ts";
23
+
24
+ /** Process-global so a re-evaluated module cannot register tsx twice. */
25
+ const TSX_REGISTERED = "__nexwireMakerTsxRegistered";
18
26
 
19
27
  const PROJECT_COMMANDS_TEMPLATE = `/**
20
28
  * Your project's maker commands.
@@ -36,8 +44,11 @@ const PROJECT_COMMANDS_TEMPLATE = `/**
36
44
  * which one. Rename yours; the built-in stays.
37
45
  * - An error in this file is reported, never swallowed. If your commands stop
38
46
  * appearing, the fault is here rather than in your command code.
39
- * - Import your own code with a normal relative path, e.g.
40
- * import { sendInvoice } from "../src/invoices/send.js";
47
+ * - This file runs as plain JavaScript under plain Node, so it cannot import
48
+ * your TypeScript source or use your tsconfig path alias. If a command
49
+ * needs either, name this file maker/commands.ts instead and delete this
50
+ * one - nexwire loads that file through tsx, with your tsconfig applied.
51
+ * For editor type support, create maker/tsconfig.json as shown in the docs.
41
52
  *
42
53
  * Replace or delete the example below once you have a command of your own.
43
54
  */
@@ -54,14 +65,47 @@ export default function register(program) {
54
65
  }
55
66
  `;
56
67
 
68
+ /**
69
+ * Why: Plain node cannot import TypeScript, so maker/commands.ts needs tsx
70
+ * registered before it is imported. Registered at most once per process
71
+ * and never unregistered: two overlapping registrations corrupt the
72
+ * loader, and commands routinely import more TypeScript at action time,
73
+ * long after this function returns. The flag lives on globalThis because
74
+ * this module can be evaluated more than once in one process (vitest's
75
+ * per-file isolation) while the loader hooks are process-wide.
76
+ * When: Only when the project's file is TypeScript - maker/commands.mjs never
77
+ * pays for tsx being installed.
78
+ * Where: utils/project-commands.mjs.
79
+ */
80
+ async function registerTsLoader(rel) {
81
+ if (globalThis[TSX_REGISTERED]) return;
82
+
83
+ try {
84
+ const { register } = await import("tsx/esm/api");
85
+ register();
86
+ } catch (error) {
87
+ throw new Error(
88
+ `${rel} needs tsx to load TypeScript, but it could not be registered: ${error?.message || error}. tsx ships as a devDependency of every nexwire project - run npm install, or rename the file to ${PROJECT_COMMANDS_PATH}.`,
89
+ { cause: error }
90
+ );
91
+ }
92
+
93
+ globalThis[TSX_REGISTERED] = true;
94
+ }
95
+
57
96
  /**
58
97
  * Why: Registers the commands a project defined for itself.
59
98
  * When: Every maker invocation, right after the framework's own registrars.
60
99
  * Where: Maker CLI entry flow, from utils/project-commands.mjs.
61
- * How: Imports maker/commands.mjs resolved from process.cwd() - not from
62
- * import.meta.dirname, which points inside node_modules and would find the
63
- * framework's own directory instead of the project. Absent is silent and
64
- * free; broken is loud, because a developer's commands silently not
100
+ * How: Imports maker/commands.mjs - or maker/commands.ts, loaded through tsx,
101
+ * which resolves the project's tsconfig path aliases - resolved from
102
+ * process.cwd(), not from import.meta.dirname, which points inside
103
+ * node_modules and would find the framework's own directory instead of the
104
+ * project. If a project keeps both files the .mjs wins and the .ts file is
105
+ * skipped with a warning naming it, so neither file is ever silently dead:
106
+ * a project that already runs must not be re-registered by a file that
107
+ * appeared later, and a file that is skipped must say so. Absent is silent
108
+ * and free; broken is loud, because a developer's commands silently not
65
109
  * existing is a far worse experience than an error message. A name that
66
110
  * collides with a framework command is refused by commander itself, which
67
111
  * is the right outcome: the built-in wins and the project cannot shadow a
@@ -70,10 +114,23 @@ export default function register(program) {
70
114
  * asked for the name.
71
115
  */
72
116
  export async function registerProjectCommands(program, rawArgs, cwd = process.cwd()) {
73
- const file = path.resolve(cwd, PROJECT_COMMANDS_PATH);
117
+ const mjsFile = path.resolve(cwd, PROJECT_COMMANDS_PATH);
118
+ const tsFile = path.resolve(cwd, PROJECT_COMMANDS_TS_PATH);
119
+ const hasMjs = fs.existsSync(mjsFile);
120
+ const hasTs = fs.existsSync(tsFile);
121
+
122
+ if (!hasMjs && !hasTs) {
123
+ return { loaded: false, file: mjsFile, added: [] };
124
+ }
125
+
126
+ const isTs = !hasMjs && hasTs;
127
+ const file = isTs ? tsFile : mjsFile;
128
+ const rel = isTs ? PROJECT_COMMANDS_TS_PATH : PROJECT_COMMANDS_PATH;
74
129
 
75
- if (!fs.existsSync(file)) {
76
- return { loaded: false, file, added: [] };
130
+ if (hasMjs && hasTs) {
131
+ console.warn(
132
+ `${PROJECT_COMMANDS_TS_PATH} is ignored because ${PROJECT_COMMANDS_PATH} exists. Keep one - delete the file you are not using.`
133
+ );
77
134
  }
78
135
 
79
136
  // Not wrapped in a bare try/catch on purpose. A file that exists but fails to
@@ -81,18 +138,22 @@ export async function registerProjectCommands(program, rawArgs, cwd = process.cw
81
138
  // command that is actually in this loader. The catches below only attach the
82
139
  // file name, because an error from inside the project file otherwise reads as
83
140
  // if it came from the framework.
141
+ if (isTs) {
142
+ await registerTsLoader(rel);
143
+ }
144
+
84
145
  let module;
85
146
  try {
86
147
  module = await import(pathToFileURL(file).href);
87
148
  } catch (error) {
88
- throw new Error(`Could not load ${PROJECT_COMMANDS_PATH}: ${error?.message || error}`, { cause: error });
149
+ throw new Error(`Could not load ${rel}: ${error?.message || error}`, { cause: error });
89
150
  }
90
151
 
91
152
  const register = module.default ?? module.register;
92
153
 
93
154
  if (typeof register !== "function") {
94
155
  throw new TypeError(
95
- `${PROJECT_COMMANDS_PATH} must export a default function (program, args) => void. Found: ${typeof register}.`
156
+ `${rel} must export a default function (program, args) => void. Found: ${typeof register}.`
96
157
  );
97
158
  }
98
159
 
@@ -101,7 +162,7 @@ export async function registerProjectCommands(program, rawArgs, cwd = process.cw
101
162
  try {
102
163
  await register(program, rawArgs);
103
164
  } catch (error) {
104
- throw new Error(`Could not register commands from ${PROJECT_COMMANDS_PATH}: ${error?.message || error}`, {
165
+ throw new Error(`Could not register commands from ${rel}: ${error?.message || error}`, {
105
166
  cause: error
106
167
  });
107
168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nexwire",
3
- "version": "4.2.1",
3
+ "version": "4.2.2",
4
4
  "type": "module",
5
5
  "description": "Scaffold a new nexwire project",
6
6
  "bin": {
@@ -235,4 +235,4 @@
235
235
  "optional": true
236
236
  }
237
237
  }
238
- }
238
+ }