@bobfrankston/npmglobalize 1.0.188 → 1.0.189

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 (2) hide show
  1. package/lib.js +115 -53
  2. package/package.json +1 -1
package/lib.js CHANGED
@@ -1975,71 +1975,132 @@ function insertTypesIntoTsconfig(text, typesArr) {
1975
1975
  // Inline single-line object: `{ "target": ... }`
1976
1976
  return text.slice(0, braceEnd) + ` "types": ${arrLiteral},` + text.slice(braceEnd);
1977
1977
  }
1978
+ /** `@types/*` packages that inject *global* declarations (usable with no
1979
+ * `import`). TS6 stopped auto-including these, so under TS6+ they must be named
1980
+ * in `compilerOptions.types`. Module-only types (`express`, `ws`, `nodemailer`,
1981
+ * ...) resolve through ordinary import resolution and never need listing — so we
1982
+ * deliberately do NOT enumerate every installed `@types/*`. `node` is always
1983
+ * added; the rest below are added only when actually installed. */
1984
+ const GLOBAL_AUGMENTING_TYPES = ['jest', 'mocha', 'jasmine', 'bun'];
1985
+ /** True if `@types/<name>` is resolvable from `start` or any ancestor's
1986
+ * `node_modules` — handles npm-workspace hoisting where `@types` live at the
1987
+ * monorepo root rather than the leaf package. */
1988
+ function atTypesInstalled(start, name) {
1989
+ let dir = path.resolve(start);
1990
+ for (;;) {
1991
+ if (fs.existsSync(path.join(dir, 'node_modules', '@types', name)))
1992
+ return true;
1993
+ const parent = path.dirname(dir);
1994
+ if (parent === dir)
1995
+ return false;
1996
+ dir = parent;
1997
+ }
1998
+ }
1999
+ /** Resolve a tsconfig's `extends` chain into an ordered list (the file itself
2000
+ * first, then each base it extends). Local (non-`node_modules`) entries are
2001
+ * marked `writable`. `truncated` is true when an `extends` could not be read or
2002
+ * resolved (e.g. a package specifier or an array) — the caller then can't be
2003
+ * sure an inherited `types` doesn't exist, so it stays its hand. Cycle- and
2004
+ * depth-guarded. */
2005
+ function resolveTsconfigChain(tsconfigPath) {
2006
+ const chain = [];
2007
+ const seen = new Set();
2008
+ let current = path.resolve(tsconfigPath);
2009
+ let truncated = false;
2010
+ for (let depth = 0; current && !seen.has(current) && depth < 16; depth++) {
2011
+ seen.add(current);
2012
+ let parsed;
2013
+ try {
2014
+ parsed = JSON5.parse(fs.readFileSync(current, 'utf-8'));
2015
+ }
2016
+ catch {
2017
+ truncated = true;
2018
+ break;
2019
+ }
2020
+ chain.push({ path: current, parsed, writable: !/[\\/]node_modules[\\/]/.test(current) });
2021
+ const ext = parsed?.extends;
2022
+ if (ext === undefined)
2023
+ break; // natural end of chain
2024
+ if (typeof ext !== 'string') {
2025
+ truncated = true;
2026
+ break;
2027
+ } // array/object — not followed
2028
+ // Resolve relative to the current file's directory. A bare/extensionless
2029
+ // value may name a directory (→ tsconfig.json) or an extensionless file.
2030
+ const baseDir = path.dirname(current);
2031
+ let next;
2032
+ if (ext.endsWith('.json'))
2033
+ next = path.resolve(baseDir, ext);
2034
+ else {
2035
+ const asDir = path.resolve(baseDir, ext, 'tsconfig.json');
2036
+ next = fs.existsSync(asDir) ? asDir : path.resolve(baseDir, ext + '.json');
2037
+ }
2038
+ if (!fs.existsSync(next)) {
2039
+ truncated = true;
2040
+ break;
2041
+ } // e.g. a node_modules package specifier
2042
+ current = next;
2043
+ }
2044
+ return { chain, truncated };
2045
+ }
1978
2046
  /** 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). */
2047
+ * `@types/*` package globally. A tsconfig with no explicit `compilerOptions.types`
2048
+ * then loses the Node globals (`process`, `Buffer`, ...), breaking the build with
2049
+ * `TS2591`. When building under TS6+, add an explicit, *minimal* `types` list
2050
+ * (Node plus any installed global-augmenting test types) to restore them.
2051
+ *
2052
+ * Generalized for monorepos: it resolves the full `extends` chain, respects a
2053
+ * `types` set anywhere in that chain, finds `@types/node` even when hoisted to a
2054
+ * workspace-root `node_modules`, and writes the patch to the *base-most writable*
2055
+ * config so one edit fixes every package that shares the base (falling back to
2056
+ * the leaf config when the base lives in `node_modules`). Idempotent and
2057
+ * conservative — bails when the chain can't be fully resolved (an inherited
2058
+ * `types` might be hiding) or when `@types/node` isn't installed. */
1985
2059
  function ensureTsconfigNodeTypes(cwd) {
1986
2060
  if (getTscMajor() < 6)
1987
2061
  return;
1988
2062
  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)
2063
+ if (!fs.existsSync(tsconfigPath))
2008
2064
  return;
2009
- // `extends` may already supply `types`; a local array would override it, so leave it.
2010
- if (parsed.extends !== undefined)
2065
+ const { chain, truncated } = resolveTsconfigChain(tsconfigPath);
2066
+ if (!chain.length)
2011
2067
  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')))
2068
+ // Respect an explicit `types` anywhere in the resolved chain.
2069
+ if (chain.some(c => c.parsed?.compilerOptions?.types !== undefined))
2016
2070
  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 {
2071
+ // Couldn't fully resolve the chain → a hidden base may set `types`; don't risk it.
2072
+ if (truncated)
2024
2073
  return;
2025
- }
2026
- if (!installed.length)
2074
+ // Only genuine Node projects, incl. workspace-hoisted @types at the repo root.
2075
+ if (!atTypesInstalled(cwd, 'node'))
2027
2076
  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"`);
2077
+ // Minimal correct list: Node globals + any installed global-augmenting types.
2078
+ const typesArr = ['node', ...GLOBAL_AUGMENTING_TYPES.filter(n => atTypesInstalled(cwd, n))];
2079
+ // Prefer the base-most writable config (DRY: one edit fixes the whole workspace),
2080
+ // then walk toward the leaf, patching the first whose compilerOptions we can edit.
2081
+ const candidates = chain.filter(c => c.writable).reverse();
2082
+ for (const cand of candidates) {
2083
+ let text;
2084
+ try {
2085
+ text = fs.readFileSync(cand.path, 'utf-8');
2086
+ }
2087
+ catch {
2088
+ continue;
2089
+ }
2090
+ const patched = insertTypesIntoTsconfig(text, typesArr);
2091
+ if (!patched)
2092
+ continue;
2093
+ try {
2094
+ fs.writeFileSync(cand.path, patched);
2095
+ const where = path.relative(cwd, cand.path) || 'tsconfig.json';
2096
+ console.log(colors.cyan(` Patched ${where}: added "types": [${typesArr.join(', ')}] (TypeScript ${_tscMajor}+ no longer auto-includes @types/*)`));
2097
+ }
2098
+ catch (error) {
2099
+ console.error(colors.yellow(` Could not write tsconfig patch (${cand.path}): ${error.message}`));
2100
+ }
2034
2101
  return;
2035
2102
  }
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
- }
2103
+ 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"`);
2043
2104
  }
2044
2105
  /** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
2045
2106
  * TypeScript project lacks a build script, run `npm run build`, record
@@ -2433,6 +2494,7 @@ function diagnoseBuildFailure(errorText, cwd) {
2433
2494
  if (!diagnosed && /error TS2591|Do you need to install type definitions for node/.test(errorText)) {
2434
2495
  console.error(colors.yellow('\n Hint: TypeScript 6 no longer auto-includes @types/*, so Node globals (process, Buffer) are unresolved.'));
2435
2496
  console.error(colors.yellow(` Fix: add "types": ["node"] to compilerOptions in ${path.join(cwd, 'tsconfig.json')}`));
2497
+ 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).'));
2436
2498
  diagnosed = true;
2437
2499
  }
2438
2500
  // 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.189",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",