@bobfrankston/npmglobalize 1.0.205 → 1.0.206

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 (3) hide show
  1. package/lib.d.ts +10 -0
  2. package/lib.js +105 -1
  3. package/package.json +3 -3
package/lib.d.ts CHANGED
@@ -25,6 +25,16 @@ export declare function clearBuildIssues(): void;
25
25
  /** Extract the first TypeScript error line from build output for the summary.
26
26
  * Returns a short string like "file.ts(42,5): error TS2339: Property 'foo' ..." */
27
27
  export declare function extractFirstTscError(output: string): string | null;
28
+ /** A TS7016 — "Could not find a declaration file for module 'X'" — that tsc blames
29
+ * on the file being compiled is frequently not that file's fault: the copy of X in
30
+ * node_modules carries no `.d.ts` whatsoever. That happens when X was published at
31
+ * a moment its declaration output was not on disk, so the tarball ships JS only and
32
+ * every consumer resolving X through the registry fails identically. tsc's stock
33
+ * advice — `npm i --save-dev @types/bobfrankston__hlib` — then sends the user after
34
+ * a types package that does not and will never exist, which is worse than no advice.
35
+ * Recognize the shape and say what actually fixes it.
36
+ * Returns one diagnosis line per untyped module; empty when TS7016 has another cause. */
37
+ export declare function diagnoseUntypedDeps(cwd: string, buildOutput: string): string[];
28
38
  /** One package that depends on this one, recorded in this package's
29
39
  * .globalize.json5 when that package publishes. */
30
40
  export interface UpstreamEntry {
package/lib.js CHANGED
@@ -79,6 +79,97 @@ export function extractFirstTscError(output) {
79
79
  }
80
80
  return null;
81
81
  }
82
+ /** Does `dir` contain any `.d.ts` at all? Bounded, and never descends into a
83
+ * nested `node_modules` — we are asking about this package's own output. */
84
+ function hasDeclarationFiles(dir, depth = 3) {
85
+ let entries;
86
+ try {
87
+ entries = fs.readdirSync(dir, { withFileTypes: true });
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ for (const e of entries) {
93
+ if (e.isFile() && e.name.endsWith('.d.ts'))
94
+ return true;
95
+ }
96
+ if (depth <= 0)
97
+ return false;
98
+ for (const e of entries) {
99
+ if (!e.isDirectory() || e.name === 'node_modules' || e.name.startsWith('.'))
100
+ continue;
101
+ if (hasDeclarationFiles(path.join(dir, e.name), depth - 1))
102
+ return true;
103
+ }
104
+ return false;
105
+ }
106
+ /** The spec a consumer declares for `name`. `.dependencies` is checked first
107
+ * because during a publish the live `file:` paths live there while
108
+ * `dependencies` temporarily holds the npm refs that were swapped in. */
109
+ function declaredDepSpec(pkg, name) {
110
+ for (const bucket of ['.dependencies', 'dependencies', 'devDependencies']) {
111
+ const spec = pkg?.[bucket]?.[name];
112
+ if (typeof spec === 'string')
113
+ return spec;
114
+ }
115
+ return '';
116
+ }
117
+ /** A TS7016 — "Could not find a declaration file for module 'X'" — that tsc blames
118
+ * on the file being compiled is frequently not that file's fault: the copy of X in
119
+ * node_modules carries no `.d.ts` whatsoever. That happens when X was published at
120
+ * a moment its declaration output was not on disk, so the tarball ships JS only and
121
+ * every consumer resolving X through the registry fails identically. tsc's stock
122
+ * advice — `npm i --save-dev @types/bobfrankston__hlib` — then sends the user after
123
+ * a types package that does not and will never exist, which is worse than no advice.
124
+ * Recognize the shape and say what actually fixes it.
125
+ * Returns one diagnosis line per untyped module; empty when TS7016 has another cause. */
126
+ export function diagnoseUntypedDeps(cwd, buildOutput) {
127
+ if (!buildOutput)
128
+ return [];
129
+ const named = new Set();
130
+ const re = /error TS7016: Could not find a declaration file for module '([^']+)'/g;
131
+ for (let m = re.exec(buildOutput); m; m = re.exec(buildOutput))
132
+ named.add(m[1]);
133
+ if (!named.size)
134
+ return [];
135
+ let consumer = {};
136
+ try {
137
+ consumer = readPackageJson(cwd);
138
+ }
139
+ catch { /* an unreadable consumer still leaves the dep diagnosable */ }
140
+ const scopeOf = (n) => n.startsWith('@') && n.includes('/') ? n.slice(0, n.indexOf('/')) : '';
141
+ const lines = [];
142
+ for (const name of named) {
143
+ if (name.startsWith('.'))
144
+ continue; // a relative import — a code bug, not a packaging one
145
+ const installed = path.join(cwd, 'node_modules', ...name.split('/'));
146
+ if (!fs.existsSync(path.join(installed, 'package.json')))
147
+ continue;
148
+ if (hasDeclarationFiles(installed))
149
+ continue; // declarations are present; TS7016 came from something else
150
+ let version = '';
151
+ try {
152
+ version = readPackageJson(installed).version || '';
153
+ }
154
+ catch { /* version is decoration here */ }
155
+ const at = version ? `@${version}` : '';
156
+ // A `file:` spec points at live source we can look at directly, which tells
157
+ // us whether the missing declarations are a build problem or a stale install.
158
+ const spec = declaredDepSpec(consumer, name);
159
+ const source = spec.startsWith('file:') ? path.resolve(cwd, spec.slice('file:'.length)) : '';
160
+ if (source && fs.existsSync(source)) {
161
+ lines.push(hasDeclarationFiles(source)
162
+ ? `${name} resolves to a copy in node_modules with no .d.ts, but its source at ${source} has them — the install is stale. Run \`npm install\` in ${cwd}.`
163
+ : `${name}'s source at ${source} emits no .d.ts — set "declaration": true in its tsconfig and rebuild it.`);
164
+ continue;
165
+ }
166
+ const ownScope = scopeOf(name) && scopeOf(name) === scopeOf(consumer?.name || '');
167
+ lines.push(ownScope
168
+ ? `${name}${at} was published with no .d.ts in the tarball, so every consumer that installs it from npm fails this way — it is not a problem in ${consumer?.name || 'this package'}. Rebuild and republish ${name} (\`npmglobalize\` in its source directory), or depend on its source with a file: path. Ignore tsc's @types/… suggestion; no such package exists.`
169
+ : `${name}${at} ships no type declarations. Install its @types package if one exists, or add a .d.ts declaring the module.`);
170
+ }
171
+ return lines;
172
+ }
82
173
  /**
83
174
  * Remove 'nul' files from a directory tree (Windows reserved name issue).
84
175
  * These files break git and npm on Windows. Uses \\?\ prefix to bypass name validation.
@@ -3218,8 +3309,21 @@ export async function buildProject(cwd, opts = {}) {
3218
3309
  if (buildOutput)
3219
3310
  console.error(buildOutput);
3220
3311
  console.error(colors.red(`Build failed in ${pkg.name || cwd}`));
3312
+ const label = pkg.name || path.basename(cwd);
3313
+ // tsc reports a missing-declarations failure against the file that imports the
3314
+ // dep, and suggests an @types package that does not exist for private scopes.
3315
+ // When the real cause is a dep installed without any .d.ts, say so plainly and
3316
+ // put THAT in the summary — the raw tsc line is already echoed above.
3317
+ const untyped = diagnoseUntypedDeps(cwd, buildOutput);
3318
+ if (untyped.length) {
3319
+ for (const line of untyped) {
3320
+ console.error(colors.yellow(` ${line}`));
3321
+ recordBuildIssue(label, 'error', line);
3322
+ }
3323
+ return false;
3324
+ }
3221
3325
  const firstErr = extractFirstTscError(buildOutput);
3222
- recordBuildIssue(pkg.name || path.basename(cwd), 'error', firstErr || 'Build failed');
3326
+ recordBuildIssue(label, 'error', firstErr || 'Build failed');
3223
3327
  return false;
3224
3328
  }
3225
3329
  /** Walk `file:` deps depth-first (deps before consumers) and build each one
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.205",
3
+ "version": "1.0.206",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  "@bobfrankston/freezepak": "^0.1.9",
35
35
  "@bobfrankston/importgen": "^0.1.40",
36
36
  "@bobfrankston/themecolors": "^0.1.8",
37
- "@bobfrankston/userconfig": "^1.0.10",
37
+ "@bobfrankston/userconfig": "^1.0.11",
38
38
  "@npmcli/package-json": "^7.0.4",
39
39
  "json5": "^2.2.3",
40
40
  "libnpmversion": "^8.0.3",
@@ -62,7 +62,7 @@
62
62
  "@bobfrankston/freezepak": "^0.1.9",
63
63
  "@bobfrankston/importgen": "^0.1.40",
64
64
  "@bobfrankston/themecolors": "^0.1.8",
65
- "@bobfrankston/userconfig": "^1.0.10",
65
+ "@bobfrankston/userconfig": "^1.0.11",
66
66
  "@npmcli/package-json": "^7.0.4",
67
67
  "json5": "^2.2.3",
68
68
  "libnpmversion": "^8.0.3",