@bobfrankston/npmglobalize 1.0.188 → 1.0.190

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/cli.js +13 -1
  2. package/lib.js +195 -60
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -8,6 +8,18 @@ import path from 'path';
8
8
  import { styleText } from 'util';
9
9
  // npmglobalize install directory (for --version)
10
10
  const __dirname = import.meta.dirname;
11
+ /** Own version, read once. Stamped into the startup banner AND the Issues Summary
12
+ * so a failure log always reveals which installed npmglobalize produced it — a
13
+ * build that fails on something the source already fixes usually just means the
14
+ * installed copy predates the fix. */
15
+ const NPMGLOBALIZE_VERSION = (() => {
16
+ try {
17
+ return JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8')).version;
18
+ }
19
+ catch {
20
+ return 'unknown';
21
+ }
22
+ })();
11
23
  function printHelp() {
12
24
  console.log(`
13
25
  npmglobalize - Transform file: dependencies to npm versions for publishing
@@ -417,7 +429,7 @@ function printBuildSummary() {
417
429
  if (issues.length === 0)
418
430
  return;
419
431
  console.log('');
420
- console.log(styleText('yellow', '━━━ Issues Summary ━━━━━━━━━━━━━━━━━━━━'));
432
+ console.log(styleText('yellow', `━━━ Issues Summary (npmglobalize v${NPMGLOBALIZE_VERSION}) ━━━`));
421
433
  for (const issue of issues) {
422
434
  const icon = issue.severity === 'error' ? '✗' : '⚠';
423
435
  console.log(styleText('yellow', ` ${icon} ${issue.module}: ${issue.message}`));
package/lib.js CHANGED
@@ -1947,12 +1947,12 @@ function getTscMajor() {
1947
1947
  catch { /* leave at 0 — can't probe, assume pre-6 (no patch) */ }
1948
1948
  return _tscMajor;
1949
1949
  }
1950
- /** Insert a `"types": [...]` line as the first property of `compilerOptions`,
1950
+ /** Insert `"<key>": <valueLiteral>` as the first property of `compilerOptions`,
1951
1951
  * preserving the file's existing formatting/comments. Returns the patched text,
1952
1952
  * or null if the object couldn't be located safely (caller leaves file alone).
1953
- * Only ever called when `compilerOptions.types` is absent. */
1954
- function insertTypesIntoTsconfig(text, typesArr) {
1955
- const arrLiteral = '[' + typesArr.map(t => JSON.stringify(t)).join(', ') + ']';
1953
+ * Only ever called when the key is known to be absent. */
1954
+ function insertCompilerOption(text, key, valueLiteral) {
1955
+ const prop = `"${key}": ${valueLiteral}`;
1956
1956
  const m = text.match(/"compilerOptions"\s*:\s*\{/);
1957
1957
  if (!m)
1958
1958
  return null;
@@ -1965,81 +1965,196 @@ function insertTypesIntoTsconfig(text, typesArr) {
1965
1965
  const lineStart = text.lastIndexOf('\n', m.index) + 1;
1966
1966
  const baseIndent = text.slice(lineStart, m.index).match(/^\s*/)[0];
1967
1967
  const propIndent = baseIndent + ' ';
1968
- return text.slice(0, braceEnd) + '\n' + propIndent + `"types": ${arrLiteral}\n` + baseIndent + text.slice(braceEnd + ws.length);
1968
+ return text.slice(0, braceEnd) + '\n' + propIndent + prop + '\n' + baseIndent + text.slice(braceEnd + ws.length);
1969
1969
  }
1970
1970
  // Multiline object: derive property indent from the existing first property.
1971
1971
  if (ws.includes('\n')) {
1972
1972
  const propIndent = ws.slice(ws.lastIndexOf('\n') + 1);
1973
- return text.slice(0, braceEnd) + '\n' + propIndent + `"types": ${arrLiteral},` + text.slice(braceEnd);
1973
+ return text.slice(0, braceEnd) + '\n' + propIndent + prop + ',' + text.slice(braceEnd);
1974
1974
  }
1975
1975
  // Inline single-line object: `{ "target": ... }`
1976
- return text.slice(0, braceEnd) + ` "types": ${arrLiteral},` + text.slice(braceEnd);
1976
+ return text.slice(0, braceEnd) + ` ${prop},` + text.slice(braceEnd);
1977
+ }
1978
+ /** Insert a `"types": [...]` line as the first property of `compilerOptions`.
1979
+ * Only ever called when `compilerOptions.types` is absent. */
1980
+ function insertTypesIntoTsconfig(text, typesArr) {
1981
+ return insertCompilerOption(text, 'types', '[' + typesArr.map(t => JSON.stringify(t)).join(', ') + ']');
1982
+ }
1983
+ /** `@types/*` packages that inject *global* declarations (usable with no
1984
+ * `import`). TS6 stopped auto-including these, so under TS6+ they must be named
1985
+ * in `compilerOptions.types`. Module-only types (`express`, `ws`, `nodemailer`,
1986
+ * ...) resolve through ordinary import resolution and never need listing — so we
1987
+ * deliberately do NOT enumerate every installed `@types/*`. `node` is always
1988
+ * added; the rest below are added only when actually installed. */
1989
+ const GLOBAL_AUGMENTING_TYPES = ['jest', 'mocha', 'jasmine', 'bun'];
1990
+ /** True if `@types/<name>` is resolvable from `start` or any ancestor's
1991
+ * `node_modules` — handles npm-workspace hoisting where `@types` live at the
1992
+ * monorepo root rather than the leaf package. */
1993
+ function atTypesInstalled(start, name) {
1994
+ let dir = path.resolve(start);
1995
+ for (;;) {
1996
+ if (fs.existsSync(path.join(dir, 'node_modules', '@types', name)))
1997
+ return true;
1998
+ const parent = path.dirname(dir);
1999
+ if (parent === dir)
2000
+ return false;
2001
+ dir = parent;
2002
+ }
2003
+ }
2004
+ /** Resolve a tsconfig's `extends` chain into an ordered list (the file itself
2005
+ * first, then each base it extends). Local (non-`node_modules`) entries are
2006
+ * marked `writable`. `truncated` is true when an `extends` could not be read or
2007
+ * resolved (e.g. a package specifier or an array) — the caller then can't be
2008
+ * sure an inherited `types` doesn't exist, so it stays its hand. Cycle- and
2009
+ * depth-guarded. */
2010
+ function resolveTsconfigChain(tsconfigPath) {
2011
+ const chain = [];
2012
+ const seen = new Set();
2013
+ let current = path.resolve(tsconfigPath);
2014
+ let truncated = false;
2015
+ for (let depth = 0; current && !seen.has(current) && depth < 16; depth++) {
2016
+ seen.add(current);
2017
+ let parsed;
2018
+ try {
2019
+ parsed = JSON5.parse(fs.readFileSync(current, 'utf-8'));
2020
+ }
2021
+ catch {
2022
+ truncated = true;
2023
+ break;
2024
+ }
2025
+ chain.push({ path: current, parsed, writable: !/[\\/]node_modules[\\/]/.test(current) });
2026
+ const ext = parsed?.extends;
2027
+ if (ext === undefined)
2028
+ break; // natural end of chain
2029
+ if (typeof ext !== 'string') {
2030
+ truncated = true;
2031
+ break;
2032
+ } // array/object — not followed
2033
+ // Resolve relative to the current file's directory. A bare/extensionless
2034
+ // value may name a directory (→ tsconfig.json) or an extensionless file.
2035
+ const baseDir = path.dirname(current);
2036
+ let next;
2037
+ if (ext.endsWith('.json'))
2038
+ next = path.resolve(baseDir, ext);
2039
+ else {
2040
+ const asDir = path.resolve(baseDir, ext, 'tsconfig.json');
2041
+ next = fs.existsSync(asDir) ? asDir : path.resolve(baseDir, ext + '.json');
2042
+ }
2043
+ if (!fs.existsSync(next)) {
2044
+ truncated = true;
2045
+ break;
2046
+ } // e.g. a node_modules package specifier
2047
+ current = next;
2048
+ }
2049
+ return { chain, truncated };
1977
2050
  }
1978
2051
  /** TypeScript 6 dropped the legacy behavior of auto-including every installed
1979
- * `@types/*` package. A tsconfig with no explicit `compilerOptions.types` then
1980
- * loses the Node globals (`process`, `Buffer`, ...), breaking the build with
1981
- * `TS2591`. When building under TS6+, add an explicit `types` list enumerating
1982
- * the installed `@types/*` (restoring the old behavior). Idempotent and
1983
- * conservative — only patches when `types` is absent, `@types/node` is actually
1984
- * installed, and there is no `extends` (whose merged `types` we can't see). */
2052
+ * `@types/*` package globally. A tsconfig with no explicit `compilerOptions.types`
2053
+ * then loses the Node globals (`process`, `Buffer`, ...), breaking the build with
2054
+ * `TS2591`. When building under TS6+, add an explicit, *minimal* `types` list
2055
+ * (Node plus any installed global-augmenting test types) to restore them.
2056
+ *
2057
+ * Generalized for monorepos: it resolves the full `extends` chain, respects a
2058
+ * `types` set anywhere in that chain, finds `@types/node` even when hoisted to a
2059
+ * workspace-root `node_modules`, and writes the patch to the *base-most writable*
2060
+ * config so one edit fixes every package that shares the base (falling back to
2061
+ * the leaf config when the base lives in `node_modules`). Idempotent and
2062
+ * conservative — bails when the chain can't be fully resolved (an inherited
2063
+ * `types` might be hiding) or when `@types/node` isn't installed. */
1985
2064
  function ensureTsconfigNodeTypes(cwd) {
1986
2065
  if (getTscMajor() < 6)
1987
2066
  return;
1988
2067
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
1989
- let text;
1990
- try {
1991
- text = fs.readFileSync(tsconfigPath, 'utf-8');
1992
- }
1993
- catch {
1994
- return;
1995
- }
1996
- let parsed;
1997
- try {
1998
- parsed = JSON5.parse(text);
1999
- }
2000
- catch {
2001
- return;
2002
- }
2003
- const co = parsed?.compilerOptions;
2004
- if (!co || typeof co !== 'object')
2068
+ if (!fs.existsSync(tsconfigPath))
2005
2069
  return;
2006
- // Respect any explicit choice; never override a user-set `types`.
2007
- if (co.types !== undefined)
2070
+ const { chain, truncated } = resolveTsconfigChain(tsconfigPath);
2071
+ if (!chain.length)
2008
2072
  return;
2009
- // `extends` may already supply `types`; a local array would override it, so leave it.
2010
- if (parsed.extends !== undefined)
2073
+ // Respect an explicit `types` anywhere in the resolved chain.
2074
+ if (chain.some(c => c.parsed?.compilerOptions?.types !== undefined))
2011
2075
  return;
2012
- // Gate on @types/node actually being installed so the list resolves and we
2013
- // only touch genuine Node projects.
2014
- const typesDir = path.join(cwd, 'node_modules', '@types');
2015
- if (!fs.existsSync(path.join(typesDir, 'node')))
2076
+ // Couldn't fully resolve the chain a hidden base may set `types`; don't risk it.
2077
+ if (truncated)
2016
2078
  return;
2017
- let installed;
2018
- try {
2019
- installed = fs.readdirSync(typesDir, { withFileTypes: true })
2020
- .filter(e => e.isDirectory() && !e.name.startsWith('.'))
2021
- .map(e => e.name);
2022
- }
2023
- catch {
2024
- return;
2025
- }
2026
- if (!installed.length)
2079
+ // Only genuine Node projects, incl. workspace-hoisted @types at the repo root.
2080
+ if (!atTypesInstalled(cwd, 'node'))
2027
2081
  return;
2028
- // node first, rest sorted, for a stable readable list.
2029
- installed.sort();
2030
- const typesArr = ['node', ...installed.filter(n => n !== 'node')];
2031
- const patched = insertTypesIntoTsconfig(text, typesArr);
2032
- if (!patched) {
2033
- recordBuildIssue(parsed.name || path.basename(cwd), 'warning', `TS6+ build may fail: couldn't locate compilerOptions in ${tsconfigPath} to add explicit "types"`);
2082
+ // Minimal correct list: Node globals + any installed global-augmenting types.
2083
+ const typesArr = ['node', ...GLOBAL_AUGMENTING_TYPES.filter(n => atTypesInstalled(cwd, n))];
2084
+ // Prefer the base-most writable config (DRY: one edit fixes the whole workspace),
2085
+ // then walk toward the leaf, patching the first whose compilerOptions we can edit.
2086
+ const candidates = chain.filter(c => c.writable).reverse();
2087
+ for (const cand of candidates) {
2088
+ let text;
2089
+ try {
2090
+ text = fs.readFileSync(cand.path, 'utf-8');
2091
+ }
2092
+ catch {
2093
+ continue;
2094
+ }
2095
+ const patched = insertTypesIntoTsconfig(text, typesArr);
2096
+ if (!patched)
2097
+ continue;
2098
+ try {
2099
+ fs.writeFileSync(cand.path, patched);
2100
+ const where = path.relative(cwd, cand.path) || 'tsconfig.json';
2101
+ console.log(colors.cyan(` Patched ${where}: added "types": [${typesArr.join(', ')}] (TypeScript ${_tscMajor}+ no longer auto-includes @types/*)`));
2102
+ }
2103
+ catch (error) {
2104
+ console.error(colors.yellow(` Could not write tsconfig patch (${cand.path}): ${error.message}`));
2105
+ }
2034
2106
  return;
2035
2107
  }
2036
- try {
2037
- fs.writeFileSync(tsconfigPath, patched);
2038
- console.log(colors.cyan(` Patched tsconfig.json: added "types": [${typesArr.join(', ')}] (TypeScript ${_tscMajor}+ no longer auto-includes @types/*)`));
2039
- }
2040
- catch (error) {
2041
- console.error(colors.yellow(` Could not write tsconfig.json patch: ${error.message}`));
2108
+ recordBuildIssue(chain[0].parsed?.name || path.basename(cwd), 'warning', `TS6+ build may fail: no writable compilerOptions block in the tsconfig chain of ${cwd} to add "types"`);
2109
+ }
2110
+ /** TS6 turned several long-deprecated compiler options (e.g.
2111
+ * `moduleResolution: "node10"`/`"node"`/`"classic"`, old `target`s) into hard
2112
+ * errors (`TS5107`/`TS5101`) that will be *removed* in TS7. TypeScript's own
2113
+ * sanctioned bridge for the 6→7 window is `"ignoreDeprecations": "<version>"`
2114
+ * (the error text names the version, e.g. `"6.0"`). We apply exactly that so the
2115
+ * build proceeds, and record a warning so the real migration (to `node16` /
2116
+ * `nodenext` / `bundler`) still happens before TS7. We do NOT auto-rewrite
2117
+ * `moduleResolution` itself — that changes resolution semantics and must be a
2118
+ * human, per-package decision. Returns true if it patched a file. Mirrors
2119
+ * `ensureTsconfigNodeTypes`: resolves the `extends` chain, respects an existing
2120
+ * `ignoreDeprecations`, and patches the base-most writable config. */
2121
+ function ensureTsconfigIgnoreDeprecations(cwd, version) {
2122
+ const tsconfigPath = path.join(cwd, 'tsconfig.json');
2123
+ if (!fs.existsSync(tsconfigPath))
2124
+ return false;
2125
+ const { chain, truncated } = resolveTsconfigChain(tsconfigPath);
2126
+ if (!chain.length)
2127
+ return false;
2128
+ // Already set somewhere in the chain (possibly with a different version) — leave it.
2129
+ if (chain.some(c => c.parsed?.compilerOptions?.ignoreDeprecations !== undefined))
2130
+ return false;
2131
+ if (truncated)
2132
+ return false;
2133
+ const valueLiteral = JSON.stringify(version);
2134
+ const candidates = chain.filter(c => c.writable).reverse();
2135
+ for (const cand of candidates) {
2136
+ let text;
2137
+ try {
2138
+ text = fs.readFileSync(cand.path, 'utf-8');
2139
+ }
2140
+ catch {
2141
+ continue;
2142
+ }
2143
+ const patched = insertCompilerOption(text, 'ignoreDeprecations', valueLiteral);
2144
+ if (!patched)
2145
+ continue;
2146
+ try {
2147
+ fs.writeFileSync(cand.path, patched);
2148
+ const where = path.relative(cwd, cand.path) || 'tsconfig.json';
2149
+ console.log(colors.cyan(` Patched ${where}: added "ignoreDeprecations": ${valueLiteral} (silences TS6 removed-in-TS7 deprecation errors)`));
2150
+ recordBuildIssue(chain[0].parsed?.name || path.basename(cwd), 'warning', `Deprecated compilerOptions silenced via ignoreDeprecations=${version}. Migrate moduleResolution to "node16"/"nodenext"/"bundler" before TypeScript 7.`);
2151
+ return true;
2152
+ }
2153
+ catch (error) {
2154
+ console.error(colors.yellow(` Could not write tsconfig patch (${cand.path}): ${error.message}`));
2155
+ }
2042
2156
  }
2157
+ return false;
2043
2158
  }
2044
2159
  /** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
2045
2160
  * TypeScript project lacks a build script, run `npm run build`, record
@@ -2075,7 +2190,17 @@ export async function buildProject(cwd, opts = {}) {
2075
2190
  }
2076
2191
  ensureTsconfigNodeTypes(cwd);
2077
2192
  console.log(`Building ${pkg.name || cwd}...`);
2078
- const buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
2193
+ let buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
2194
+ if (!buildResult.success) {
2195
+ // TS6 deprecation error (removed-in-TS7): apply the version TS itself names
2196
+ // in `"ignoreDeprecations": "<v>"` and retry once.
2197
+ const out = (buildResult.stderr || '') + (buildResult.output || '');
2198
+ const dep = out.match(/error TS510[17]\b/) &&
2199
+ out.match(/['"]ignoreDeprecations['"]\s*:\s*['"]([\d.]+)['"]/);
2200
+ if (dep && ensureTsconfigIgnoreDeprecations(cwd, dep[1])) {
2201
+ buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
2202
+ }
2203
+ }
2079
2204
  if (buildResult.success) {
2080
2205
  console.log(colors.green(`✓ Build succeeded (${pkg.name || path.basename(cwd)})`));
2081
2206
  return true;
@@ -2433,6 +2558,16 @@ function diagnoseBuildFailure(errorText, cwd) {
2433
2558
  if (!diagnosed && /error TS2591|Do you need to install type definitions for node/.test(errorText)) {
2434
2559
  console.error(colors.yellow('\n Hint: TypeScript 6 no longer auto-includes @types/*, so Node globals (process, Buffer) are unresolved.'));
2435
2560
  console.error(colors.yellow(` Fix: add "types": ["node"] to compilerOptions in ${path.join(cwd, 'tsconfig.json')}`));
2561
+ console.error(colors.yellow(' In a workspace this belongs in the shared base tsconfig the package extends (only global @types like node/jest need listing; imported types resolve on their own).'));
2562
+ diagnosed = true;
2563
+ }
2564
+ // Pattern: TS6 made deprecated options (moduleResolution=node10, ...) hard errors,
2565
+ // removed in TS7. (Safety net for when the auto ignoreDeprecations patch+retry didn't fire.)
2566
+ if (!diagnosed && /error TS510[17]\b/.test(errorText)) {
2567
+ const v = errorText.match(/['"]ignoreDeprecations['"]\s*:\s*['"]([\d.]+)['"]/);
2568
+ console.error(colors.yellow('\n Hint: TypeScript 6 flags long-deprecated options that TS7 will remove.'));
2569
+ console.error(colors.yellow(` Bridge: add "ignoreDeprecations": "${v ? v[1] : '6.0'}" to compilerOptions (shared base in a workspace).`));
2570
+ console.error(colors.yellow(' Real fix before TS7: migrate moduleResolution to "node16"/"nodenext"/"bundler".'));
2436
2571
  diagnosed = true;
2437
2572
  }
2438
2573
  // Pattern: TypeScript compilation error
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.188",
3
+ "version": "1.0.190",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",