pob 40.1.0 → 40.3.0

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.
@@ -5,6 +5,7 @@ import { removeFromGitIndex } from "../../utils/git.js";
5
5
  import inMonorepo from "../../utils/inMonorepo.js";
6
6
  import removeLegacyGeneratedDocs from "../../utils/legacyGeneratedDocs.js";
7
7
  import * as packageUtils from "../../utils/package.js";
8
+ import { detectStorybookReactNative } from "../../utils/storybook.js";
8
9
  import { appIgnorePaths } from "./ignorePaths.js";
9
10
 
10
11
  const appsWithTypescript = [
@@ -95,6 +96,13 @@ export default class PobAppGenerator extends Generator {
95
96
  delete config.ci;
96
97
  }
97
98
 
99
+ const storybookDefault = detectStorybookReactNative([
100
+ this.fs.readJSON(this.destinationPath("package.json"), {}),
101
+ ]);
102
+ if (config?.type === "expo" && config.storybook === undefined) {
103
+ config.storybook = storybookDefault;
104
+ }
105
+
98
106
  if (config && this.options.updateOnly) {
99
107
  this.config.set("app", this.appConfig);
100
108
  this.config.save();
@@ -151,7 +159,7 @@ export default class PobAppGenerator extends Generator {
151
159
  type: "confirm",
152
160
  name: "storybook",
153
161
  message: "Do you want storybook ?",
154
- default: (config && config.storybook) || false,
162
+ default: config?.storybook ?? storybookDefault,
155
163
  when: (values) => values.type === "expo",
156
164
  },
157
165
  ]);
@@ -315,7 +323,8 @@ export default class PobAppGenerator extends Generator {
315
323
  this.composeWith("pob:common:format-lint", {
316
324
  isApp: true,
317
325
  documentation: false,
318
- storybook: pkg?.devDependencies?.storybook,
326
+ // only asked for expo apps: the on-device react-native storybook
327
+ storybookReactNative: this.appConfig.storybook,
319
328
  testing: this.appConfig.testing,
320
329
  testRunner: this.appConfig.testRunner,
321
330
  babel,
@@ -5,6 +5,7 @@ import { runTransform } from "@pob/codemods";
5
5
  import { format } from "oxfmt";
6
6
  import semver from "semver";
7
7
  import Generator from "yeoman-generator";
8
+ import hasDesignSync from "../../../utils/designSync.js";
8
9
  import { quoteArg } from "../../../utils/execUtils.js";
9
10
  import inMonorepo from "../../../utils/inMonorepo.js";
10
11
  import * as packageUtils from "../../../utils/package.js";
@@ -160,6 +161,13 @@ export default class CommonFormatLintGenerator extends Generator {
160
161
  description: "Enable storybook",
161
162
  });
162
163
 
164
+ this.option("storybookReactNative", {
165
+ type: Boolean,
166
+ required: false,
167
+ default: false,
168
+ description: "Enable the on-device react-native storybook",
169
+ });
170
+
163
171
  this.option("rootAsSrc", {
164
172
  type: Boolean,
165
173
  required: false,
@@ -566,6 +574,20 @@ export default class CommonFormatLintGenerator extends Generator {
566
574
  ignorePatterns.add("/rollup.config.mjs");
567
575
  }
568
576
 
577
+ if (this.options.storybook) {
578
+ ignorePatterns.add("**/.storybook/");
579
+ ignorePatterns.add("**/storybook-static/");
580
+ }
581
+ if (this.options.storybookReactNative) {
582
+ ignorePatterns.add("**/.rnstorybook/");
583
+ }
584
+
585
+ if (hasDesignSync(this)) {
586
+ ignorePatterns.add("/.design-sync/");
587
+ ignorePatterns.add("/.ds-sync/");
588
+ ignorePatterns.add("/ds-bundle/");
589
+ }
590
+
569
591
  if (rootIgnorePaths) {
570
592
  rootIgnorePaths.forEach((ignorePath) => {
571
593
  if (ignorePath.startsWith("#")) return;
@@ -703,7 +725,7 @@ export default class CommonFormatLintGenerator extends Generator {
703
725
  ? `${packageManagerExecWithCwd(this.options.packageManager, "../..", "oxlint")} ${args} ${quoteArg(path.relative("../..", "."))}`
704
726
  : `oxlint ${args}`,
705
727
  lint: `${packageManagerRun(this.options.packageManager, "lint:oxlint")} && ${
706
- useTypescript && !composite ? "tsc && " : ""
728
+ useTypescript && !composite ? "tsrs && " : ""
707
729
  }${packageManagerRun(this.options.packageManager, "lint:eslint")}`,
708
730
  });
709
731
 
@@ -178,7 +178,7 @@ export default class CommonTranspilerGenerator extends Generator {
178
178
  if (bundler && bundler.startsWith("rollup")) {
179
179
  return `${packageManagerRun(this.options.packageManager, "clean:build")} && rollup --config rollup.config.mjs --watch`;
180
180
  }
181
- if (bundler === "tsc") return "tsc --watch";
181
+ if (bundler === "tsc") return "tsrs --watch";
182
182
  if (bundler === "esbuild") return "pob-esbuild-watch";
183
183
  })(),
184
184
  });
@@ -194,7 +194,7 @@ export default class CommonTranspilerGenerator extends Generator {
194
194
  if (bundler && bundler.startsWith("rollup")) {
195
195
  return `${packageManagerRun(this.options.packageManager, "clean:build")} && rollup --config rollup.config.mjs`;
196
196
  }
197
- if (bundler === "tsc") return "tsc";
197
+ if (bundler === "tsc") return "tsrs";
198
198
  if (bundler === "esbuild") return "pob-esbuild-build";
199
199
  })(),
200
200
  });
@@ -203,7 +203,7 @@ export default class CommonTranspilerGenerator extends Generator {
203
203
  !this.options.isApp && withTypescript && bundler !== "tsc";
204
204
 
205
205
  packageUtils.addOrRemoveScripts(pkg, shouldBuildDefinitions, {
206
- "build:definitions": "tsc -p tsconfig.json",
206
+ "build:definitions": "tsrs -p tsconfig.json",
207
207
  });
208
208
 
209
209
  if (shouldBuildDefinitions) {
@@ -218,7 +218,7 @@ export default class CommonTranspilerGenerator extends Generator {
218
218
  if (this.fs.exists(this.destinationPath("lib/index.d.ts"))) {
219
219
  packageUtils.addScripts(pkg, {
220
220
  "build:definitions":
221
- "tsc --lib esnext --noEmit --skipLibCheck ./lib/index.d.ts",
221
+ "tsrs --lib esnext --noEmit --skipLibCheck ./lib/index.d.ts",
222
222
  build: packageManagerRun(
223
223
  this.options.packageManager,
224
224
  "build:definitions",
@@ -229,7 +229,7 @@ export default class CommonTranspilerGenerator extends Generator {
229
229
  if (this.fs.exists(this.destinationPath("lib/index.ts"))) {
230
230
  packageUtils.addScripts(pkg, {
231
231
  "build:definitions":
232
- "tsc --lib esnext --noEmit --skipLibCheck ./lib/index.ts",
232
+ "tsrs --lib esnext --noEmit --skipLibCheck ./lib/index.ts",
233
233
  build: packageManagerRun(
234
234
  this.options.packageManager,
235
235
  "build:definitions",
@@ -740,7 +740,9 @@ export default class CommonTranspilerGenerator extends Generator {
740
740
  }
741
741
 
742
742
  end() {
743
- if (this.bundler) {
743
+ // when updated from the monorepo root, dependencies are not installed yet:
744
+ // the root builds after installing
745
+ if (this.bundler && !this.options.fromPob) {
744
746
  this.spawnCommandSync(
745
747
  packageManagerRun(this.options.packageManager, "build"),
746
748
  );
@@ -222,12 +222,13 @@ export default class CommonTypescriptGenerator extends Generator {
222
222
 
223
223
  // "typescript" is aliased to the typescript 6 api package, still required by
224
224
  // typescript-eslint and typedoc. "@typescript/native" is typescript 7, which
225
- // provides the "tsc" binary.
225
+ // provides the "tsc" binary, kept for the "tsc:go" script. "tsrs" is the
226
+ // rust port of typescript 7, used by all the other scripts.
226
227
  packageUtils.addOrRemoveDevDependencies(
227
228
  pkg,
228
229
  this.options.enable ||
229
230
  this.fs.exists(this.destinationPath("lib/index.d.ts")),
230
- ["typescript", "@typescript/native"],
231
+ ["typescript", "@typescript/native", "tsrs"],
231
232
  );
232
233
 
233
234
  const tsconfigPath = this.destinationPath("tsconfig.json");
@@ -401,11 +402,15 @@ export default class CommonTypescriptGenerator extends Generator {
401
402
  }
402
403
 
403
404
  if (!pkg.workspaces || !this.options.enable) {
405
+ // tsrs and tsc do not share their .tsbuildinfo: switching between them
406
+ // triggers a full rebuild
407
+ const tscCommand = (compiler) =>
408
+ inMonorepo || !this.options.enable
409
+ ? compiler
410
+ : `${compiler} && ${compiler} -p tsconfig.tools.json`;
404
411
  packageUtils.addOrRemoveScripts(pkg, this.options.enable, {
405
- tsc:
406
- inMonorepo || !this.options.enable
407
- ? "tsc"
408
- : "tsc && tsc -p tsconfig.tools.json",
412
+ tsc: tscCommand("tsrs"),
413
+ "tsc:go": tscCommand("tsc"),
409
414
  });
410
415
  }
411
416
 
@@ -1,4 +1,5 @@
1
1
  import Generator from "yeoman-generator";
2
+ import hasDesignSync from "../../../utils/designSync.js";
2
3
 
3
4
  export default class CoreGitignoreGenerator extends Generator {
4
5
  constructor(args, opts) {
@@ -82,6 +83,7 @@ export default class CoreGitignoreGenerator extends Generator {
82
83
  typescript: withBabel || this.options.typescript,
83
84
  paths: this.options.paths,
84
85
  playwright: this.options.playwright,
86
+ designSync: hasDesignSync(this),
85
87
  });
86
88
  }
87
89
 
@@ -44,6 +44,12 @@ yarn-error.log*
44
44
  /test-results
45
45
  /playwright-report
46
46
  <% } -%>
47
+ <% if (designSync) { -%>
48
+
49
+ # Claude Design sync
50
+ /.ds-sync/
51
+ /ds-bundle/
52
+ <% } -%>
47
53
 
48
54
  #*
49
55
  *$
@@ -69,6 +69,24 @@ export default class CoreVSCodeGenerator extends Generator {
69
69
  async writing() {
70
70
  if (this.options.root) {
71
71
  const pkg = this.fs.readJSON(this.destinationPath("package.json"));
72
+ // the tsdk script runs tsrs from node_modules, which pnp does not have
73
+ const withTsrs =
74
+ this.options.typescript && this.options.yarnNodeLinker !== "pnp";
75
+ const tsdkPath = this.destinationPath(".vscode/tsrs-tsdk/tsgo");
76
+
77
+ if (withTsrs) {
78
+ // the extension runs it directly, so it must be executable
79
+ this.fs.write(
80
+ tsdkPath,
81
+ this.fs.read(this.templatePath("tsrs-tsdk-tsgo")),
82
+ {
83
+ stat: /** @type {import("node:fs").Stats} */ ({ mode: 0o755 }),
84
+ },
85
+ );
86
+ } else {
87
+ this.fs.delete(tsdkPath);
88
+ }
89
+
72
90
  await copyAndFormatTpl(
73
91
  this.fs,
74
92
  this.templatePath("extensions.json.ejs"),
@@ -90,6 +108,7 @@ export default class CoreVSCodeGenerator extends Generator {
90
108
  bun: this.options.packageManager === "bun",
91
109
  pnpm: this.options.packageManager === "pnpm",
92
110
  typescript: this.options.typescript,
111
+ tsrs: withTsrs,
93
112
  testing: this.options.testing,
94
113
  testRunner: this.options.testRunner,
95
114
  module: pkg.type === "module",
@@ -17,6 +17,16 @@
17
17
  // typescript 7 is provided by the TypeScriptTeam.native-preview extension.
18
18
  // "typescript" resolves to the typescript 6 api package, which has no
19
19
  // tsserver, so it cannot be used as "typescript.tsdk".
20
+ <% if (tsrs) { -%>
21
+ // The extension runs tsrs, the same compiler as the "tsc" script, through
22
+ // .vscode/tsrs-tsdk/tsgo once its "TypeScript 7 tsdk" prompt is allowed.
23
+ // Without the prompt, use "Select TypeScript Version…", which offers the
24
+ // "additionalLocations" entry.
25
+ "js/ts.experimental.useTsgo": true,
26
+ "js/ts.tsdk.path": ".vscode/tsrs-tsdk",
27
+ "js/ts.tsdk.additionalLocations": [".vscode/tsrs-tsdk"],
28
+ "js/ts.enablePromptUseWorkspaceTsdk": true,
29
+ <% } -%>
20
30
  <% } -%>
21
31
 
22
32
  <% if (yarn) { -%>
@@ -0,0 +1,9 @@
1
+ #!/bin/sh
2
+ # Generated by pob: do not edit, it is overwritten on each pob run.
3
+ # "js/ts.tsdk.path" points to this directory, so that the TypeScript 7
4
+ # extension (TypeScriptTeam.native-preview) runs the project's tsrs instead of
5
+ # its bundled typescript 7. The extension silently falls back to the bundled
6
+ # one when this fails: tsrs not installed, no tsrs binary for the platform
7
+ # (Windows, Intel macOS) or node not in VS Code's PATH.
8
+ # Check with: ps -axo command | grep -- '--lsp --stdio'
9
+ exec node "$(dirname "$0")/../../node_modules/tsrs/bin/tsrs" "$@"
@@ -365,7 +365,6 @@ export default class PobLibGenerator extends Generator {
365
365
  documentation:
366
366
  !!this.pobjson.documentation ||
367
367
  !!(this.pobjson.testing && this.pobjson.testing.codecov),
368
- storybook: pkg?.devDependencies?.storybook,
369
368
  testing: !!this.pobjson.testing,
370
369
  testRunner: inMonorepo
371
370
  ? inMonorepo.pobMonorepoConfig.testRunner
@@ -9,6 +9,10 @@ import {
9
9
  workspacesRun,
10
10
  workspacesRunSequential,
11
11
  } from "../../utils/packageManagerWorkspacesUtils.js";
12
+ import {
13
+ detectStorybook,
14
+ detectStorybookReactNative,
15
+ } from "../../utils/storybook.js";
12
16
  import {
13
17
  buildDependenciesMaps,
14
18
  buildTopologicalOrderBatches,
@@ -152,7 +156,16 @@ export default class PobMonorepoGenerator extends Generator {
152
156
  }
153
157
 
154
158
  async prompting() {
155
- const config = this.config.get("monorepo");
159
+ const storedConfig = this.config.get("monorepo");
160
+ const allPackages = [
161
+ this.fs.readJSON(this.destinationPath("package.json"), {}),
162
+ ...this.packages,
163
+ ];
164
+ const config = storedConfig && {
165
+ storybook: detectStorybook(allPackages),
166
+ storybookReactNative: detectStorybookReactNative(allPackages),
167
+ ...storedConfig,
168
+ };
156
169
 
157
170
  if (this.options.updateOnly && config) {
158
171
  this.pobMonorepoConfig = config;
@@ -232,6 +245,20 @@ export default class PobMonorepoGenerator extends Generator {
232
245
  message: "Would you like eslint in monorepo ?",
233
246
  default: config ? config.eslint : true,
234
247
  },
248
+ {
249
+ type: "confirm",
250
+ name: "storybook",
251
+ message: "Would you like storybook ?",
252
+ default: config ? config.storybook : detectStorybook(allPackages),
253
+ },
254
+ {
255
+ type: "confirm",
256
+ name: "storybookReactNative",
257
+ message: "Would you like the on-device react-native storybook ?",
258
+ default: config
259
+ ? config.storybookReactNative
260
+ : detectStorybookReactNative(allPackages),
261
+ },
235
262
  ]);
236
263
  this.pobMonorepoConfig.packageNames = this.packageNames;
237
264
  this.config.set("monorepo", this.pobMonorepoConfig);
@@ -320,7 +347,8 @@ export default class PobMonorepoGenerator extends Generator {
320
347
  this.composeWith("pob:common:format-lint", {
321
348
  monorepo: true,
322
349
  documentation: this.pobMonorepoConfig.documentation,
323
- storybook: pkg?.devDependencies?.storybook,
350
+ storybook: this.pobMonorepoConfig.storybook,
351
+ storybookReactNative: this.pobMonorepoConfig.storybookReactNative,
324
352
  typescript: this.pobMonorepoConfig.typescript,
325
353
  build: hasRootBuild,
326
354
  testing: this.pobMonorepoConfig.testing,
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync } from "node:fs";
1
+ import { readdirSync } from "node:fs";
2
2
  import Generator from "yeoman-generator";
3
3
  import { latestLTS } from "../../../utils/nodeVersions.js";
4
4
  import * as packageUtils from "../../../utils/package.js";
@@ -6,6 +6,9 @@ import { packageManagerRun } from "../../../utils/packageManagerUtils.js";
6
6
  import { copyAndFormatTpl } from "../../../utils/writeAndFormat.js";
7
7
 
8
8
  const rootConfigExtensionsRegex = /\.config\.(?:ts|mts|cts)$/;
9
+ // .js scripts count too: tsc then fails on them, which flags their migration
10
+ // to typescript
11
+ const scriptExtensionsRegex = /\.[cm]?[jt]s$/;
9
12
 
10
13
  const hasRootConfigFiles = (rootPath) => {
11
14
  try {
@@ -17,6 +20,17 @@ const hasRootConfigFiles = (rootPath) => {
17
20
  }
18
21
  };
19
22
 
23
+ // a scripts/ dir with only shell scripts gives tsc no input (TS18003)
24
+ const hasScriptFiles = (scriptsPath) => {
25
+ try {
26
+ return readdirSync(scriptsPath, { recursive: true }).some((name) =>
27
+ scriptExtensionsRegex.test(name),
28
+ );
29
+ } catch {
30
+ return false;
31
+ }
32
+ };
33
+
20
34
  export default class MonorepoTypescriptGenerator extends Generator {
21
35
  constructor(args, opts) {
22
36
  super(args, opts);
@@ -75,7 +89,7 @@ export default class MonorepoTypescriptGenerator extends Generator {
75
89
  #hasTools() {
76
90
  return (
77
91
  hasRootConfigFiles(this.destinationPath()) ||
78
- existsSync(this.destinationPath("scripts"))
92
+ hasScriptFiles(this.destinationPath("scripts"))
79
93
  );
80
94
  }
81
95
 
@@ -101,22 +115,26 @@ export default class MonorepoTypescriptGenerator extends Generator {
101
115
 
102
116
  // "typescript" is aliased to the typescript 6 api package, still required by
103
117
  // typescript-eslint and typedoc. "@typescript/native" is typescript 7, which
104
- // provides the "tsc" binary.
118
+ // provides the "tsc" binary, kept for the "tsc:go" script. "tsrs" is the
119
+ // rust port of typescript 7, used by all the other scripts.
105
120
  packageUtils.addOrRemoveDevDependencies(
106
121
  pkg,
107
122
  pkg.name === "@pob/eslint-config-monorepo" || this.options.enable,
108
- ["typescript", "@typescript/native"],
123
+ ["typescript", "@typescript/native", "tsrs"],
109
124
  );
110
125
 
111
126
  if (this.options.enable) {
127
+ // tsrs and tsc do not share their .tsbuildinfo: switching between them
128
+ // triggers a full rebuild
112
129
  packageUtils.addOrRemoveScripts(pkg, this.#hasTypescriptProjects(), {
113
- tsc: "tsc -b",
130
+ tsc: "tsrs -b",
131
+ "tsc:go": "tsc -b",
114
132
  });
115
133
  packageUtils.addOrRemoveScripts(
116
134
  pkg,
117
135
  !this.options.isAppProject && !this.options.checkOnly,
118
136
  {
119
- "build:definitions": "tsc -b",
137
+ "build:definitions": "tsrs -b",
120
138
  },
121
139
  );
122
140
 
@@ -134,6 +152,7 @@ export default class MonorepoTypescriptGenerator extends Generator {
134
152
  }
135
153
  } else if (pkg.scripts) {
136
154
  delete pkg.scripts.tsc;
155
+ delete pkg.scripts["tsc:go"];
137
156
  if (
138
157
  pkg.scripts.postbuild === "tsc -b tsconfig.build.json" ||
139
158
  pkg.scripts.postbuild === "tsc -b"
@@ -186,7 +186,11 @@ export default class MonorepoWorkspacesGenerator extends Generator {
186
186
  );
187
187
  }
188
188
 
189
- end() {
189
+ // packages are updated in the install phase, after root files are written and
190
+ // before the root install of the end phase (see CorePnpmGenerator,
191
+ // CoreYarnGenerator, CoreBunGenerator), so the lockfile includes the
192
+ // dependencies updated in packages
193
+ install() {
190
194
  this.packagePaths.forEach((packagePath) => {
191
195
  if (
192
196
  !existsSync(`${packagePath}/.yo-rc.json`) &&
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Whether the project uses Claude Design sync: its tracked config lives in
3
+ * ".design-sync", next to the untracked ".ds-sync" cache and "ds-bundle"
4
+ * build output.
5
+ *
6
+ * @param {import("yeoman-generator").default} generator
7
+ */
8
+ export default function hasDesignSync(generator) {
9
+ return generator.fs.exists(
10
+ generator.destinationPath(".design-sync/config.json"),
11
+ );
12
+ }
@@ -1,3 +1,4 @@
1
+ import pobDependencies from "pob-dependencies";
1
2
  import semver from "semver";
2
3
  import { latestLTS, maintenanceLTS } from "./nodeVersions.js";
3
4
  import * as packageUtils from "./package.js";
@@ -26,22 +27,31 @@ export const getEnvsMinNodeMajor = (envs, onlyLatestLTS) => {
26
27
  };
27
28
 
28
29
  /**
29
- * Min node major of a range, undefined when missing or not parseable
30
+ * Min version of a range, undefined when missing or not parseable
30
31
  * (workspace:*, catalog:, npm: alias...).
31
32
  */
32
- export const getMinMajor = (range) => {
33
+ const getMinVersion = (range) => {
33
34
  if (!range) return undefined;
34
35
  try {
35
- const minVersion = semver.minVersion(range);
36
- return minVersion ? semver.major(minVersion) : undefined;
36
+ return semver.minVersion(range) ?? undefined;
37
37
  } catch {
38
38
  return undefined;
39
39
  }
40
40
  };
41
41
 
42
42
  /**
43
- * engines.node value for a min node major. When envs are not defined, an
44
- * already higher engines.node is kept.
43
+ * Min node major of a range, undefined when missing or not parseable
44
+ * (workspace:*, catalog:, npm: alias...).
45
+ */
46
+ export const getMinMajor = (range) => {
47
+ const minVersion = getMinVersion(range);
48
+ return minVersion ? semver.major(minVersion) : undefined;
49
+ };
50
+
51
+ /**
52
+ * engines.node value for a min node major. An engines.node with a higher min
53
+ * version is kept, to guarantee a node version with a security fix: within
54
+ * the min major when envs are defined, any higher version otherwise.
45
55
  */
46
56
  export const resolveEnginesNode = (
47
57
  currentEnginesNode,
@@ -56,11 +66,13 @@ export const resolveEnginesNode = (
56
66
  throw new Error(`Invalid min node version: ${minNodeMajor}`);
57
67
  }
58
68
 
59
- if (!hasEnvs) {
60
- const currentMinMajor = getMinMajor(currentEnginesNode);
61
- if (currentMinMajor !== undefined && currentMinMajor > Number(major)) {
62
- return currentEnginesNode;
63
- }
69
+ const currentMinVersion = getMinVersion(currentEnginesNode);
70
+ if (
71
+ currentMinVersion &&
72
+ semver.gt(currentMinVersion, minVersion) &&
73
+ (!hasEnvs || semver.major(currentMinVersion) === Number(major))
74
+ ) {
75
+ return currentEnginesNode;
64
76
  }
65
77
 
66
78
  return `>=${minVersion}`;
@@ -68,7 +80,9 @@ export const resolveEnginesNode = (
68
80
 
69
81
  /**
70
82
  * Updates "@types/node" when it is lower than engines.node min major.
71
- * Never adds it, never lowers devDependencies.
83
+ * Never adds it, never lowers devDependencies. A pinned devDependency stays
84
+ * pinned (check-package-dependencies/require-pinned-versions), with the
85
+ * pob-dependencies version when it satisfies the min major.
72
86
  */
73
87
  export const updateTypesNodeFromEngines = (pkg) => {
74
88
  const minMajor = getMinMajor(pkg.engines?.node);
@@ -83,7 +97,12 @@ export const updateTypesNodeFromEngines = (pkg) => {
83
97
  if (currentDevVersion) {
84
98
  const currentMinMajor = getMinMajor(currentDevVersion);
85
99
  if (currentMinMajor !== undefined && currentMinMajor < minMajor) {
86
- pkg.devDependencies["@types/node"] = range;
100
+ const pinnedVersion = pobDependencies["@types/node"];
101
+ pkg.devDependencies["@types/node"] =
102
+ semver.valid(currentDevVersion) &&
103
+ semver.major(pinnedVersion) >= minMajor
104
+ ? pinnedVersion
105
+ : range;
87
106
  }
88
107
  }
89
108
  };
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
+ import pobDependencies from "pob-dependencies";
2
3
  import {
3
4
  resolveEnginesNode,
4
5
  updateNodeEngines,
@@ -6,13 +7,24 @@ import {
6
7
  } from "./nodeEngines.js";
7
8
 
8
9
  describe("updateTypesNodeFromEngines", () => {
9
- it("updates devDependencies when lower than engines min major", () => {
10
+ it("keeps pinned devDependencies pinned when lower than engines min major", () => {
10
11
  const pkg = {
11
12
  engines: { node: ">=24.14.1" },
12
13
  devDependencies: { "@types/node": "22.5.0" },
13
14
  };
14
15
  updateTypesNodeFromEngines(pkg);
15
- expect(pkg.devDependencies["@types/node"]).toBe(">=24.0.0");
16
+ expect(pkg.devDependencies["@types/node"]).toBe(
17
+ pobDependencies["@types/node"],
18
+ );
19
+ });
20
+
21
+ it("uses a range when the pinned version is lower than engines min major", () => {
22
+ const pkg = {
23
+ engines: { node: ">=99.0.0" },
24
+ devDependencies: { "@types/node": "22.5.0" },
25
+ };
26
+ updateTypesNodeFromEngines(pkg);
27
+ expect(pkg.devDependencies["@types/node"]).toBe(">=99.0.0");
16
28
  });
17
29
 
18
30
  it("keeps devDependencies with the same major", () => {
@@ -114,6 +126,17 @@ describe("resolveEnginesNode", () => {
114
126
  it("lets envs lower engines.node", () => {
115
127
  expect(resolveEnginesNode(">=24.14.1", "22", true)).toBe(">=22.18.0");
116
128
  });
129
+
130
+ it.each([true, false])(
131
+ "keeps a higher min version of the same major (hasEnvs: %s)",
132
+ (hasEnvs) => {
133
+ expect(resolveEnginesNode(">=24.15.3", "24", hasEnvs)).toBe(">=24.15.3");
134
+ },
135
+ );
136
+
137
+ it("raises a lower min version of the same major", () => {
138
+ expect(resolveEnginesNode(">=24.0.0", "24", true)).toBe(">=24.14.1");
139
+ });
117
140
  });
118
141
 
119
142
  describe("updateNodeEngines", () => {
@@ -124,7 +147,9 @@ describe("updateNodeEngines", () => {
124
147
  };
125
148
  updateNodeEngines(pkg, {});
126
149
  expect(pkg.engines.node).toBe(">=24.14.1");
127
- expect(pkg.devDependencies["@types/node"]).toBe(">=24.0.0");
150
+ expect(pkg.devDependencies["@types/node"]).toBe(
151
+ pobDependencies["@types/node"],
152
+ );
128
153
  });
129
154
 
130
155
  it("supports babelEnvs", () => {
@@ -0,0 +1,26 @@
1
+ // Fallbacks for the storybook options of a config missing them.
2
+
3
+ /**
4
+ * @param {Record<string, any>[]} pkgs
5
+ * @param {(name: string) => boolean} test
6
+ */
7
+ const someDependency = (pkgs, test) =>
8
+ pkgs.some((pkg) =>
9
+ [pkg.dependencies, pkg.devDependencies].some(
10
+ (dependencies) => dependencies && Object.keys(dependencies).some(test),
11
+ ),
12
+ );
13
+
14
+ // a web storybook depends on its framework (@storybook/react-vite,
15
+ // @storybook/nextjs...): "storybook" alone is also a dependency of the
16
+ // react-native one
17
+ const webFrameworkRegExp =
18
+ /^@storybook\/(?:.+-(?:vite|webpack5)|nextjs|angular|ember|sveltekit)$/;
19
+
20
+ /** @param {Record<string, any>[]} pkgs */
21
+ export const detectStorybook = (pkgs) =>
22
+ someDependency(pkgs, (name) => webFrameworkRegExp.test(name));
23
+
24
+ /** @param {Record<string, any>[]} pkgs */
25
+ export const detectStorybookReactNative = (pkgs) =>
26
+ someDependency(pkgs, (name) => name === "@storybook/react-native");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pob",
3
- "version": "40.1.0",
3
+ "version": "40.3.0",
4
4
  "description": "Pile of bones, library generator with git/babel/typescript/typedoc/readme/vitest",
5
5
  "keywords": [
6
6
  "skeleton"
@@ -31,13 +31,13 @@
31
31
  "./package.json": "./package.json"
32
32
  },
33
33
  "dependencies": {
34
- "@pob/codemods": "0.18.0",
34
+ "@pob/codemods": "0.18.1",
35
35
  "@pob/eslint-config": "68.0.0",
36
36
  "@pob/eslint-config-typescript-react": "68.0.0",
37
37
  "@pob/sort-object": "11.2.0",
38
38
  "@pob/sort-pkg": "13.2.0",
39
39
  "@types/inquirer": "9.0.10",
40
- "@yeoman/adapter": "4.0.2",
40
+ "@yeoman/adapter": "4.0.3",
41
41
  "eslint": "10.12.0",
42
42
  "findup-sync": "^5.0.0",
43
43
  "git-remote-url": "^1.0.1",
@@ -51,18 +51,19 @@
51
51
  "minimist": "1.2.8",
52
52
  "oxfmt": "0.72.0",
53
53
  "parse-author": "2.0.0",
54
- "pob-dependencies": "28.1.0",
54
+ "pob-dependencies": "28.2.0",
55
55
  "semver": "7.8.5",
56
56
  "typescript": "npm:@typescript/typescript6@6.0.2",
57
57
  "validate-npm-package-name": "^8.0.0",
58
58
  "yeoman-environment": "6.3.0",
59
- "yeoman-generator": "10.0.1"
59
+ "yeoman-generator": "10.3.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@pob/root": "28.1.0",
62
+ "@pob/root": "28.2.1",
63
63
  "@types/node": "24.19.1",
64
64
  "@typescript/native": "npm:typescript@7.0.2",
65
- "@yeoman/types": "1.13.1"
65
+ "@yeoman/types": "1.13.1",
66
+ "tsrs": "0.18.0-ts7.1.0-dev.20260929"
66
67
  },
67
68
  "engines": {
68
69
  "node": ">=24.14.1"
@@ -72,10 +73,11 @@
72
73
  },
73
74
  "scripts": {
74
75
  "build": "pnpm run build:definitions",
75
- "build:definitions": "tsc -p tsconfig.json",
76
+ "build:definitions": "tsrs -p tsconfig.json",
76
77
  "lint": "pnpm run lint:oxlint && pnpm run lint:eslint",
77
78
  "lint:eslint": "pnpm --dir '../..' exec eslint --quiet 'packages/pob'",
78
79
  "lint:oxlint": "pnpm --dir '../..' exec oxlint --quiet 'packages/pob'",
79
- "tsc": "tsc"
80
+ "tsc": "tsrs",
81
+ "tsc:go": "tsc"
80
82
  }
81
83
  }