@bobfrankston/npmglobalize 1.0.185 → 1.0.187

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 (4) hide show
  1. package/README.md +12 -0
  2. package/lib.d.ts +1 -0
  3. package/lib.js +183 -0
  4. package/package.json +5 -5
package/README.md CHANGED
@@ -585,6 +585,18 @@ Cycle-safe via a shared visited set; each project is built at most once per run.
585
585
 
586
586
  This complements the existing publish cascade (which ensures version refs are correct) by closing the build-freshness gap that `npm install` alone left open.
587
587
 
588
+ #### TypeScript 6 `types` auto-fix
589
+
590
+ TypeScript 6 dropped the legacy behavior of auto-including every installed `@types/*` package. A `tsconfig.json` with no explicit `compilerOptions.types` then loses the Node globals (`process`, `Buffer`, …) and the build fails with `TS2591`.
591
+
592
+ Before each build, when the global `tsc` is version 6 or newer, `npmglobalize` patches the project's `tsconfig.json` to add an explicit `types` list — enumerating the installed `@types/*` packages (e.g. `"types": ["node", …]`) — restoring the old behavior. The edit is:
593
+
594
+ - **Conservative** — only applied when `compilerOptions.types` is **absent**, `node_modules/@types/node` is actually installed, and there is no `extends` (whose merged `types` can't be seen). An explicit `types` you already set is never overridden.
595
+ - **Format-preserving** — the single `types` key is inserted into the existing `compilerOptions` block; comments, ordering, and indentation are left intact.
596
+ - **Idempotent** — once the list is present, subsequent runs skip it.
597
+
598
+ If the patch can't be applied for some reason and the build still fails with `TS2591`, the failure summary prints a hint to add `"types": ["node"]` manually.
599
+
588
600
  ### The `.dependencies` Backup (Internal/Transient)
589
601
 
590
602
  **You should never see `.dependencies` in your `package.json` under normal operation.** It is a temporary internal backup that exists only during the brief publish cycle and is removed automatically when the cycle completes.
package/lib.d.ts CHANGED
@@ -296,6 +296,7 @@ export declare function parseVersionTag(tag: string): number[] | null;
296
296
  export declare function compareVersions(a: number[], b: number[]): number;
297
297
  /** Fix version/tag mismatches */
298
298
  export declare function fixVersionTagMismatch(cwd: string, pkg: any, verbose?: boolean): boolean;
299
+ export declare function printPnpmSuggestionSummary(): void;
299
300
  /** Return declared deps (dependencies + devDependencies) that don't resolve from
300
301
  * `pkgDir`. Skips `workspace:`/`link:` specs (handled by workspace tooling /
301
302
  * rarely used). `file:` deps are checked: npm installs them as junctions
package/lib.js CHANGED
@@ -29,6 +29,7 @@ function spawnSafe(cmd, args, options = {}) {
29
29
  return spawnSync(cmd, args, opts);
30
30
  }
31
31
  import readline from 'readline';
32
+ import { styleText } from 'util';
32
33
  import libversion from 'libnpmversion';
33
34
  import JSON5 from 'json5';
34
35
  import { fileURLToPath } from 'url';
@@ -1666,6 +1667,59 @@ export function fixVersionTagMismatch(cwd, pkg, verbose = false) {
1666
1667
  function sleepSync(ms) {
1667
1668
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1668
1669
  }
1670
+ /** User-requested pnpm-eval hook (2026-05-14). The first local `npm install`
1671
+ * failure in a process prints a yellow-on-red banner and pauses 60s so the
1672
+ * user can read it. Every failure (first and subsequent) is captured in
1673
+ * `_pnpmFailureContexts` so `printPnpmSuggestionSummary()` can re-emit the
1674
+ * banner — listing all failure sites — at end-of-run, in case the live
1675
+ * banner scrolled past. Remove once pnpm-vs-npm decision is made. */
1676
+ let _pnpmSuggestionShown = false;
1677
+ const _pnpmFailureContexts = [];
1678
+ function _pnpmBanner(contexts) {
1679
+ const width = 76;
1680
+ const wrap = (s) => styleText(['yellow', 'bgRed', 'bold'], ' ' + s.padEnd(width - 2) + ' ');
1681
+ const body = [
1682
+ '',
1683
+ contexts.length > 1
1684
+ ? `npm install FAILED -- ${contexts.length} sites -- consider trying pnpm`
1685
+ : 'npm install FAILED -- consider trying pnpm',
1686
+ '',
1687
+ ];
1688
+ for (const c of contexts) {
1689
+ const label = ` - ${c}`;
1690
+ body.push(label.length > width - 2 ? label.slice(0, width - 5) + '...' : label);
1691
+ }
1692
+ body.push('', 'pnpm uses a content-addressed store with strict node_modules and', 'much faster installs. Commands mirror npm; great workspace support.', '', 'Try: npm i -g pnpm && pnpm import && pnpm install', '');
1693
+ return body.map(wrap);
1694
+ }
1695
+ async function suggestPnpmOnInstallFailure(context) {
1696
+ _pnpmFailureContexts.push(context);
1697
+ if (_pnpmSuggestionShown)
1698
+ return;
1699
+ _pnpmSuggestionShown = true;
1700
+ const lines = _pnpmBanner([context]);
1701
+ console.error('');
1702
+ for (const l of lines)
1703
+ console.error(l);
1704
+ console.error(styleText(['yellow', 'bgRed', 'bold'], ' ' + 'Pausing 60s so you can read this. Ctrl+C to abort.'.padEnd(74) + ' '));
1705
+ console.error('');
1706
+ await new Promise(resolve => setTimeout(resolve, 60_000));
1707
+ }
1708
+ /** Re-emit the pnpm banner at end-of-run if any `npm install` failed.
1709
+ * No-op when nothing failed. Safe to call from multiple summary paths —
1710
+ * guarded so the summary banner prints at most once per process. */
1711
+ let _pnpmSummaryPrinted = false;
1712
+ export function printPnpmSuggestionSummary() {
1713
+ if (_pnpmSummaryPrinted)
1714
+ return;
1715
+ if (_pnpmFailureContexts.length === 0)
1716
+ return;
1717
+ _pnpmSummaryPrinted = true;
1718
+ console.error('');
1719
+ for (const l of _pnpmBanner(_pnpmFailureContexts))
1720
+ console.error(l);
1721
+ console.error('');
1722
+ }
1669
1723
  /** Wait for a package version to appear on the npm registry.
1670
1724
  * First-time publishes (brand-new package name) take much longer to
1671
1725
  * propagate than version bumps — npm has no cached metadata to update,
@@ -1828,6 +1882,7 @@ export async function ensureFileDepModules(cwd, verbose = false, visited = new S
1828
1882
  console.error(colors.red(` ✗ npm install failed in ${abs}`));
1829
1883
  if (r.stderr)
1830
1884
  console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
1885
+ await suggestPnpmOnInstallFailure(`ensureFileDepModules (cwd): ${abs}`);
1831
1886
  }
1832
1887
  }
1833
1888
  for (const key of ['dependencies', 'devDependencies']) {
@@ -1865,12 +1920,127 @@ export async function ensureFileDepModules(cwd, verbose = false, visited = new S
1865
1920
  console.error(colors.red(` ✗ npm install failed in ${target}`));
1866
1921
  if (r.stderr)
1867
1922
  console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
1923
+ await suggestPnpmOnInstallFailure(`ensureFileDepModules (file: dep ${name}): ${target}`);
1868
1924
  }
1869
1925
  }
1870
1926
  await ensureFileDepModules(target, verbose, visited);
1871
1927
  }
1872
1928
  }
1873
1929
  }
1930
+ /** Cached major version of the global `tsc`. null = not yet probed,
1931
+ * 0 = probe failed / tsc not found. TypeScript 6 stopped auto-including
1932
+ * every `@types/*` package, so a tsconfig that relied on that needs an
1933
+ * explicit `types` list under TS6+. */
1934
+ let _tscMajor = null;
1935
+ /** Major version of the globally-installed `tsc`, or 0 if undeterminable.
1936
+ * Cached for the run. Needs shell:true on Windows to resolve `tsc.cmd`. */
1937
+ function getTscMajor() {
1938
+ if (_tscMajor !== null)
1939
+ return _tscMajor;
1940
+ _tscMajor = 0;
1941
+ try {
1942
+ const result = spawnSafe('tsc', ['--version'], { stdio: 'pipe', shell: true, env: process.env });
1943
+ const m = (result.stdout || '').match(/Version\s+(\d+)\./);
1944
+ if (m)
1945
+ _tscMajor = parseInt(m[1], 10);
1946
+ }
1947
+ catch { /* leave at 0 — can't probe, assume pre-6 (no patch) */ }
1948
+ return _tscMajor;
1949
+ }
1950
+ /** Insert a `"types": [...]` line as the first property of `compilerOptions`,
1951
+ * preserving the file's existing formatting/comments. Returns the patched text,
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(', ') + ']';
1956
+ const m = text.match(/"compilerOptions"\s*:\s*\{/);
1957
+ if (!m)
1958
+ return null;
1959
+ const braceEnd = m.index + m[0].length;
1960
+ const rest = text.slice(braceEnd);
1961
+ const ws = rest.match(/^\s*/)[0];
1962
+ // Empty object: `compilerOptions: {}` (or whitespace only before `}`)
1963
+ if (rest.slice(ws.length).startsWith('}')) {
1964
+ // Indent one level deeper than the `compilerOptions` line itself.
1965
+ const lineStart = text.lastIndexOf('\n', m.index) + 1;
1966
+ const baseIndent = text.slice(lineStart, m.index).match(/^\s*/)[0];
1967
+ const propIndent = baseIndent + ' ';
1968
+ return text.slice(0, braceEnd) + '\n' + propIndent + `"types": ${arrLiteral}\n` + baseIndent + text.slice(braceEnd + ws.length);
1969
+ }
1970
+ // Multiline object: derive property indent from the existing first property.
1971
+ if (ws.includes('\n')) {
1972
+ const propIndent = ws.slice(ws.lastIndexOf('\n') + 1);
1973
+ return text.slice(0, braceEnd) + '\n' + propIndent + `"types": ${arrLiteral},` + text.slice(braceEnd);
1974
+ }
1975
+ // Inline single-line object: `{ "target": ... }`
1976
+ return text.slice(0, braceEnd) + ` "types": ${arrLiteral},` + text.slice(braceEnd);
1977
+ }
1978
+ /** 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). */
1985
+ function ensureTsconfigNodeTypes(cwd) {
1986
+ if (getTscMajor() < 6)
1987
+ return;
1988
+ 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')
2005
+ return;
2006
+ // Respect any explicit choice; never override a user-set `types`.
2007
+ if (co.types !== undefined)
2008
+ return;
2009
+ // `extends` may already supply `types`; a local array would override it, so leave it.
2010
+ if (parsed.extends !== undefined)
2011
+ 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')))
2016
+ 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)
2027
+ 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"`);
2034
+ return;
2035
+ }
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}`));
2042
+ }
2043
+ }
1874
2044
  /** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
1875
2045
  * TypeScript project lacks a build script, run `npm run build`, record
1876
2046
  * failures. Returns true if build succeeded (or was skipped because no
@@ -1903,6 +2073,7 @@ export async function buildProject(cwd, opts = {}) {
1903
2073
  return true;
1904
2074
  }
1905
2075
  }
2076
+ ensureTsconfigNodeTypes(cwd);
1906
2077
  console.log(`Building ${pkg.name || cwd}...`);
1907
2078
  const buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
1908
2079
  if (buildResult.success) {
@@ -1993,6 +2164,7 @@ export async function ensureWorkspaceDepModules(rootDir, members, verbose = fals
1993
2164
  console.error(colors.red(` ✗ npm install failed in ${root}`));
1994
2165
  if (r.stderr)
1995
2166
  console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
2167
+ await suggestPnpmOnInstallFailure(`ensureWorkspaceDepModules: ${root}`);
1996
2168
  }
1997
2169
  }
1998
2170
  /** Run npm install -g with retries for registry propagation delay.
@@ -2256,6 +2428,13 @@ function diagnoseBuildFailure(errorText, cwd) {
2256
2428
  console.error(colors.yellow(' Check file ownership and that no other process has files locked.'));
2257
2429
  diagnosed = true;
2258
2430
  }
2431
+ // Pattern: TS6 dropped @types/* auto-inclusion → Node globals missing.
2432
+ // (Safety net for when the proactive ensureTsconfigNodeTypes patch didn't fire.)
2433
+ if (!diagnosed && /error TS2591|Do you need to install type definitions for node/.test(errorText)) {
2434
+ console.error(colors.yellow('\n Hint: TypeScript 6 no longer auto-includes @types/*, so Node globals (process, Buffer) are unresolved.'));
2435
+ console.error(colors.yellow(` Fix: add "types": ["node"] to compilerOptions in ${path.join(cwd, 'tsconfig.json')}`));
2436
+ diagnosed = true;
2437
+ }
2259
2438
  // Pattern: TypeScript compilation error
2260
2439
  if (!diagnosed && /error TS\d+/.test(errorText)) {
2261
2440
  console.error(colors.yellow('\n Hint: TypeScript compilation errors. Fix the type errors above before publishing.'));
@@ -4111,6 +4290,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4111
4290
  // workspaces[] array order; rewrite to topological order so deps build
4112
4291
  // before consumers (avoids stale-.d.ts errors in cross-package imports).
4113
4292
  const restoreWorkspaces = reorderWorkspacesForBuild(cwd, pkg, verbose);
4293
+ ensureTsconfigNodeTypes(cwd);
4114
4294
  try {
4115
4295
  // Always capture output so we can extract tsc errors for the summary
4116
4296
  const buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
@@ -4788,6 +4968,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4788
4968
  console.log(` ${colors.yellow('!')} ${s}`);
4789
4969
  console.log(colors.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
4790
4970
  console.log('');
4971
+ printPnpmSuggestionSummary();
4791
4972
  return true;
4792
4973
  }
4793
4974
  // Skip if private
@@ -5901,6 +6082,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
5901
6082
  }
5902
6083
  console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
5903
6084
  console.log('');
6085
+ printPnpmSuggestionSummary();
5904
6086
  // Only show "To use run" message if package provides commands (has bin field)
5905
6087
  if (finalPkg.bin) {
5906
6088
  const commandName = finalPkg.name.includes('/') ? finalPkg.name.split('/')[1] : finalPkg.name;
@@ -6254,6 +6436,7 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
6254
6436
  }
6255
6437
  console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
6256
6438
  console.log('');
6439
+ printPnpmSuggestionSummary();
6257
6440
  // Global install of the workspace root (monorepo CLI)
6258
6441
  const { install = false, link = false, wsl = false, dryRun = false, verbose = false } = options;
6259
6442
  const rootPkgFinal = readPackageJson(rootDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.185",
3
+ "version": "1.0.187",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@bobfrankston/freezepak": "^0.1.8",
35
- "@bobfrankston/importgen": "^0.1.36",
36
- "@bobfrankston/themecolors": "^0.1.6",
35
+ "@bobfrankston/importgen": "^0.1.37",
36
+ "@bobfrankston/themecolors": "^0.1.7",
37
37
  "@bobfrankston/userconfig": "^1.0.9",
38
38
  "@npmcli/package-json": "^7.0.4",
39
39
  "json5": "^2.2.3",
@@ -60,8 +60,8 @@
60
60
  ".transformedSnapshot": {
61
61
  "dependencies": {
62
62
  "@bobfrankston/freezepak": "^0.1.8",
63
- "@bobfrankston/importgen": "^0.1.36",
64
- "@bobfrankston/themecolors": "^0.1.6",
63
+ "@bobfrankston/importgen": "^0.1.37",
64
+ "@bobfrankston/themecolors": "^0.1.7",
65
65
  "@bobfrankston/userconfig": "^1.0.9",
66
66
  "@npmcli/package-json": "^7.0.4",
67
67
  "json5": "^2.2.3",