stonyx 0.2.3-alpha.47 → 0.2.3-alpha.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ stonyx serve
22
22
  The `new` command walks you through module selection and generates a ready-to-run project. From there, the CLI handles bootstrapping:
23
23
 
24
24
  ```bash
25
- stonyx serve # Bootstrap + run app.js
25
+ stonyx serve # Bootstrap + run app.ts (or app.js)
26
26
  stonyx test # Bootstrap + run tests
27
27
  stonyx help # Show all available commands
28
28
  ```
package/dist/cli/help.js CHANGED
@@ -23,8 +23,8 @@ export default async function help({ args, builtInCommands, loadModuleCommands }
23
23
  }
24
24
  console.log('\nAliases: s=serve, t=test, n=new, h=help\n');
25
25
  console.log('Project conventions:\n');
26
- console.log(' Entry point: app.js');
27
- console.log(' Config: config/environment.js');
26
+ console.log(' Entry point: app.ts (or app.js)');
27
+ console.log(' Config: config/environment.ts (or .js)');
28
28
  console.log(' DB schema: config/db-schema.js');
29
29
  console.log(' Models: models/');
30
30
  console.log(' Serializers: serializers/');
@@ -18,4 +18,26 @@ export declare const CONFIG_NOT_FOUND_PREFIX = "Config not found:";
18
18
  * both produced the byte-identical `Config not found: .../config/environment.js`.
19
19
  */
20
20
  export declare const CONFIG_NOT_LOADABLE_PREFIX = "Config present but not loadable:";
21
+ /**
22
+ * Did this runtime refuse OUR config file, or something our config imported?
23
+ *
24
+ * `FILE_TYPE_REFUSAL_CODES.has(code)` alone cannot tell. A config that loaded
25
+ * and ran perfectly well, but whose own nested `import()` hit a `.ts` under
26
+ * `node_modules`, throws the SAME code — and was reported as "this Node runtime
27
+ * refused to load your config", naming a file that is fine. That misdirection
28
+ * points the consumer at the one file that is not the problem, and the likeliest
29
+ * instance of it is the exact `.ts`-under-node_modules case this loader's own
30
+ * docs tell people to look for.
31
+ *
32
+ * Returns true when the refusal names our path, or when no path could be
33
+ * extracted at all (unknown message shape — keep re-framing rather than
34
+ * silently losing the loud error that invariant I2 exists for).
35
+ *
36
+ * Exported ONLY so its branches can be reached directly. Driving it through
37
+ * `importConfig` reaches the two path-comparison outcomes and nothing else:
38
+ * the "no path extractable" fallback and the `realPath` catch both survived
39
+ * mutation with the suite green. A predicate inside the guard that makes
40
+ * failures loud does not get to have branches no test can reach.
41
+ */
42
+ export declare function refusalIsAboutTheConfig(message: string, configPath: string): boolean;
21
43
  export declare function importConfig<T = unknown>(basePath: string): Promise<T>;
@@ -1,5 +1,5 @@
1
- import { existsSync } from 'fs';
2
- import { pathToFileURL } from 'url';
1
+ import { existsSync, realpathSync } from 'fs';
2
+ import { fileURLToPath, pathToFileURL } from 'url';
3
3
  /**
4
4
  * Extensions `importConfig` will load, in preference order.
5
5
  *
@@ -18,10 +18,18 @@ import { pathToFileURL } from 'url';
18
18
  */
19
19
  const LOADABLE_EXTENSIONS = ['ts', 'js'];
20
20
  /**
21
- * Extensions a consumer plausibly writes a config as, that this loader will not
22
- * read. Their only purpose is to be DETECTED: see `CONFIG_NOT_LOADABLE_PREFIX`.
21
+ * Extensions a consumer plausibly writes a config as, that this loader does not
22
+ * RESOLVE. Their only purpose is to be DETECTED: see `CONFIG_NOT_LOADABLE_PREFIX`.
23
+ *
24
+ * Named for what is actually true of them. `.mjs`, `.cjs` and `.json` are all
25
+ * perfectly readable by Node — `import()` handles every one of them today.
26
+ * Nothing here is unreadable; they are simply not in `LOADABLE_EXTENSIONS`, so
27
+ * this loader never forms a path with them, and a consumer who writes one gets
28
+ * "config present but not loadable" instead of a silent "not found". The old
29
+ * name, UNREADABLE_EXTENSIONS, asserted a property of Node that is false, and
30
+ * invited a future reader to "fix" it by adding them to the resolver.
23
31
  */
24
- const UNREADABLE_EXTENSIONS = ['mts', 'cts', 'tsx', 'jsx', 'mjs', 'cjs', 'json'];
32
+ const UNRESOLVED_EXTENSIONS = ['mts', 'cts', 'tsx', 'jsx', 'mjs', 'cjs', 'json'];
25
33
  /**
26
34
  * "There is no config here." Callers are allowed to treat this as benign —
27
35
  * `src/main.ts` does, for the optional `test/config/environment.*` override.
@@ -58,10 +66,69 @@ const FILE_TYPE_REFUSAL_CODES = new Set([
58
66
  'ERR_UNKNOWN_FILE_EXTENSION',
59
67
  'ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING',
60
68
  ]);
69
+ /**
70
+ * The first absolute POSIX path named in a Node error message.
71
+ *
72
+ * Both refusal codes name the file they refused, but in different shapes —
73
+ * measured on node v24.13.0:
74
+ *
75
+ * ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING
76
+ * Stripping types is currently unsupported for files under node_modules,
77
+ * for "/private/tmp/x/node_modules/dep/thing.ts"
78
+ * ERR_UNKNOWN_FILE_EXTENSION
79
+ * Unknown file extension ".ts" for /private/tmp/x/environment.ts
80
+ *
81
+ * The second names a bare extension BEFORE it names the file, which is why
82
+ * this requires a leading `/` rather than matching the first quoted token.
83
+ * Neither error exposes the path as an own property: `Object.keys(error)` is
84
+ * `['code']` for both, so the message is the only source.
85
+ *
86
+ * Deliberately POSIX-only. A Windows path is simply not extracted, and
87
+ * `refusalIsAboutTheConfig` treats "no path extracted" as "cannot tell" and
88
+ * keeps the pre-existing behaviour, so this degrades rather than misfires.
89
+ */
90
+ const REFUSED_PATH_PATTERN = /(?:^|[\s"'(])((?:file:\/\/)?\/[^\s"'()]+\.[A-Za-z0-9]+)/;
91
+ /** Resolves symlinks so `/tmp/...` and `/private/tmp/...` compare equal. */
92
+ function realPath(path) {
93
+ try {
94
+ return realpathSync(path);
95
+ }
96
+ catch {
97
+ return path;
98
+ }
99
+ }
100
+ /**
101
+ * Did this runtime refuse OUR config file, or something our config imported?
102
+ *
103
+ * `FILE_TYPE_REFUSAL_CODES.has(code)` alone cannot tell. A config that loaded
104
+ * and ran perfectly well, but whose own nested `import()` hit a `.ts` under
105
+ * `node_modules`, throws the SAME code — and was reported as "this Node runtime
106
+ * refused to load your config", naming a file that is fine. That misdirection
107
+ * points the consumer at the one file that is not the problem, and the likeliest
108
+ * instance of it is the exact `.ts`-under-node_modules case this loader's own
109
+ * docs tell people to look for.
110
+ *
111
+ * Returns true when the refusal names our path, or when no path could be
112
+ * extracted at all (unknown message shape — keep re-framing rather than
113
+ * silently losing the loud error that invariant I2 exists for).
114
+ *
115
+ * Exported ONLY so its branches can be reached directly. Driving it through
116
+ * `importConfig` reaches the two path-comparison outcomes and nothing else:
117
+ * the "no path extractable" fallback and the `realPath` catch both survived
118
+ * mutation with the suite green. A predicate inside the guard that makes
119
+ * failures loud does not get to have branches no test can reach.
120
+ */
121
+ export function refusalIsAboutTheConfig(message, configPath) {
122
+ const refused = REFUSED_PATH_PATTERN.exec(message)?.[1];
123
+ if (!refused)
124
+ return true;
125
+ const refusedPath = refused.startsWith('file://') ? fileURLToPath(refused) : refused;
126
+ return refusedPath === configPath || realPath(refusedPath) === realPath(configPath);
127
+ }
61
128
  export async function importConfig(basePath) {
62
129
  const matches = LOADABLE_EXTENSIONS.filter(ext => existsSync(`${basePath}.${ext}`));
63
130
  if (matches.length === 0) {
64
- const declined = UNREADABLE_EXTENSIONS.filter(ext => existsSync(`${basePath}.${ext}`));
131
+ const declined = UNRESOLVED_EXTENSIONS.filter(ext => existsSync(`${basePath}.${ext}`));
65
132
  if (declined.length > 0) {
66
133
  throw new Error(`${CONFIG_NOT_LOADABLE_PREFIX} ${basePath}.${declined.join(` and ${basePath}.`)} ` +
67
134
  `exists, but Stonyx loads ${basePath}.ts or ${basePath}.js only. ` +
@@ -79,12 +146,29 @@ export async function importConfig(basePath) {
79
146
  }
80
147
  catch (error) {
81
148
  const code = error?.code;
82
- if (code && FILE_TYPE_REFUSAL_CODES.has(code)) {
149
+ if (code && FILE_TYPE_REFUSAL_CODES.has(code) && refusalIsAboutTheConfig(error.message, path)) {
150
+ // Both files exist AND the `.ts` was refused. This is the one state where
151
+ // the both-present warning printed a few lines above is actively wrong
152
+ // advice: it says "delete the .js", and here the `.js` is the only file
153
+ // this runtime can read. Say so explicitly rather than leaving the
154
+ // consumer to reconcile two messages from the same call.
155
+ //
156
+ // It is also a deliberate behaviour change. `stonyx@0.2.3-beta.95`
157
+ // resolved `${basePath}.js` unconditionally, so this exact state LOADED
158
+ // and booted; measured against that loader, same fixture, plain node:
159
+ // it returned the `.js` default export with no warning. Preferring `.ts`
160
+ // is the point of #105, and the population most likely to hold both
161
+ // files is the one that worked around #105 by renaming to `.js` while
162
+ // the postinstall kept writing the `.ts` stub. So this message has to
163
+ // carry the "this used to work" fact, not just the remedy.
83
164
  const sibling = matches.length > 1
84
- ? ` A ${basePath}.js also exists; .ts wins by design, so this is not resolved by leaving the .js in place.`
165
+ ? ` ${basePath}.js also exists and this runtime CAN read it, but .ts wins the preference order` +
166
+ ` so the .js is never reached. Disregard the "delete the .js" warning above — in THIS state the` +
167
+ ` fix is the opposite one: remove or compile ${basePath}.ts.` +
168
+ ` (stonyx@0.2.3-beta.95 loaded the .js here; preferring .ts is a deliberate change — abofs/stonyx#105.)`
85
169
  : '';
86
170
  throw new Error(`${CONFIG_NOT_LOADABLE_PREFIX} ${path} exists, but this Node runtime refused to load it ` +
87
- `(${code}): ${error.message}${sibling}`, { cause: error });
171
+ `(${code}): ${error.message}.${sibling}`, { cause: error });
88
172
  }
89
173
  throw error;
90
174
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stonyx",
3
- "version": "0.2.3-alpha.47",
3
+ "version": "0.2.3-alpha.48",
4
4
  "description": "Base stonyx framework module",
5
5
  "main": "dist/main.js",
6
6
  "types": "dist/main.d.ts",