pob 38.10.0 → 38.12.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.
Files changed (51) hide show
  1. package/lib/generators/app/PobAppGenerator.js +24 -5
  2. package/lib/generators/app/vite/AppViteGenerator.js +50 -3
  3. package/lib/generators/app/vite/templates/index.html.ejs +11 -0
  4. package/lib/generators/app/vite/templates/index.ts.ejs +1 -0
  5. package/lib/generators/app/vite/templates/vite.config.js.ejs +8 -0
  6. package/lib/generators/common/babel/templates/app.rollup.config.mjs.ejs +1 -0
  7. package/lib/generators/common/babel/templates/lib.rollup.config.mjs.ejs +1 -0
  8. package/lib/generators/common/e2e/CommonE2eGenerator.js +6 -1
  9. package/lib/generators/common/format-lint/CommonLintGenerator.js +142 -85
  10. package/lib/generators/common/format-lint/templates/oxfmtrc.jsonc.ejs +1 -0
  11. package/lib/generators/common/husky/templates/lint-staged.config.js.txt +1 -0
  12. package/lib/generators/common/release/templates/workflow-release.yml.ejs +2 -1
  13. package/lib/generators/common/testing/templates/vite.config.js.ejs +1 -0
  14. package/lib/generators/common/transpiler/templates/app.rollup.config.mjs.ejs +1 -0
  15. package/lib/generators/common/transpiler/templates/lib.rollup.config.mjs.ejs +1 -0
  16. package/lib/generators/common/typescript/templates/tsconfig.check-js.json.ejs +1 -0
  17. package/lib/generators/common/typescript/templates/tsconfig.json.ejs +1 -0
  18. package/lib/generators/common/typescript/templates/tsconfig.tools.json.ejs +1 -0
  19. package/lib/generators/core/bun/templates/bunfig.toml.ejs +1 -0
  20. package/lib/generators/core/ci/templates/github-action-documentation-workflow.yml.ejs +2 -1
  21. package/lib/generators/core/ci/templates/github-action-push-workflow-split.yml.ejs +6 -5
  22. package/lib/generators/core/ci/templates/github-action-push-workflow.yml.ejs +2 -1
  23. package/lib/generators/core/claude/CoreClaudeGenerator.js +1 -1
  24. package/lib/generators/core/editorconfig/templates/editorconfig +1 -0
  25. package/lib/generators/core/git/CoreGitGenerator.js +72 -57
  26. package/lib/generators/core/git/generators/github/CoreGitGithubGenerator.js +192 -145
  27. package/lib/generators/core/gitignore/templates/gitignore.ejs +1 -0
  28. package/lib/generators/core/npm/templates/npmignore.ejs +1 -0
  29. package/lib/generators/core/package/CorePackageGenerator.js +21 -8
  30. package/lib/generators/core/pnpm/CorePnpmGenerator.js +30 -9
  31. package/lib/generators/core/vscode/templates/extensions.json.ejs +1 -0
  32. package/lib/generators/core/vscode/templates/settings.json.ejs +1 -0
  33. package/lib/generators/core/yarn/templates/yarn_gitignore.ejs +1 -0
  34. package/lib/generators/lib/doc/LibDocGenerator.js +3 -1
  35. package/lib/generators/lib/doc/templates/tsconfig.doc.json.ejs +1 -0
  36. package/lib/generators/lib/doc/templates/tsconfig.doc.json.lerna.ejs +1 -0
  37. package/lib/generators/lib/readme/LibReadmeGenerator.js +2 -2
  38. package/lib/generators/monorepo/PobMonorepoGenerator.js +26 -5
  39. package/lib/generators/monorepo/templates/monorepo.rollup.config.mjs.ejs +1 -0
  40. package/lib/generators/monorepo/typescript/MonorepoTypescriptGenerator.js +21 -10
  41. package/lib/generators/monorepo/typescript/templates/tsconfig.json.ejs +1 -0
  42. package/lib/generators/monorepo/typescript/templates/tsconfig.tools.json.ejs +1 -0
  43. package/lib/generators/monorepo/workspaces/MonorepoWorkspacesGenerator.js +16 -8
  44. package/lib/generators/pob/PobBaseGenerator.js +1 -1
  45. package/lib/pob.js +49 -12
  46. package/lib/utils/git.js +34 -0
  47. package/lib/utils/git.test.js +71 -0
  48. package/lib/utils/workspaceUtils.js +10 -0
  49. package/lib/utils/workspaceUtils.test.js +15 -0
  50. package/package.json +13 -13
  51. package/lib/generators/common/format-lint/templates/eslint.config.js.ejs +0 -5
@@ -14,6 +14,8 @@ const minimumReleaseAgeExcludePackages = [
14
14
  "nightingale-logger",
15
15
  ];
16
16
 
17
+ const trustedBuilds = ["esbuild"];
18
+
17
19
  export default class CorePnpmGenerator extends Generator {
18
20
  constructor(args, opts) {
19
21
  super(args, opts);
@@ -39,7 +41,7 @@ export default class CorePnpmGenerator extends Generator {
39
41
  if (
40
42
  pkg.packageManager &&
41
43
  (!pkg.packageManager.startsWith("pnpm@") ||
42
- lt(pkg.packageManager.slice("pnpm@".length), "11.0.0"))
44
+ lt(pkg.packageManager.slice("pnpm@".length), "12.0.0"))
43
45
  ) {
44
46
  delete pkg.packageManager;
45
47
  }
@@ -53,14 +55,25 @@ export default class CorePnpmGenerator extends Generator {
53
55
  });
54
56
  const config = loadedConfig ?? {};
55
57
 
56
- if (config.allowBuilds) {
57
- config.allowBuilds = Object.fromEntries(
58
- Object.entries(config.allowBuilds).map(([key, value]) => [
59
- key,
60
- value === "true" ? true : value,
61
- ]),
62
- );
58
+ // the file is read with FAILSAFE_SCHEMA: booleans are read as strings.
59
+ // Other values are placeholders written by pnpm for ignored builds.
60
+ const allowBuilds = Object.fromEntries(
61
+ Object.entries(config.allowBuilds ?? {}).map(([key, value]) => {
62
+ if (value === "true") return [key, true];
63
+ if (value === "false") return [key, false];
64
+ return [key, value];
65
+ }),
66
+ );
67
+ // build scripts of the dependencies pob adds itself. Set even before a
68
+ // package needs them: the root is generated before its packages, and an
69
+ // ignored build makes pnpm fail.
70
+ for (const name of trustedBuilds) {
71
+ if (typeof allowBuilds[name] !== "boolean") allowBuilds[name] = true;
63
72
  }
73
+ config.allowBuilds = allowBuilds;
74
+ this.pendingAllowBuilds = Object.keys(allowBuilds).filter(
75
+ (name) => typeof allowBuilds[name] !== "boolean",
76
+ );
64
77
 
65
78
  if (pkg.workspaces) {
66
79
  config.packages = pkg.workspaces;
@@ -91,7 +104,15 @@ export default class CorePnpmGenerator extends Generator {
91
104
 
92
105
  end() {
93
106
  if (this.options.enable) {
94
- this.spawnSync("pnpm", ["install"], {});
107
+ if (this.pendingAllowBuilds.length > 0) {
108
+ console.warn(
109
+ `pnpm-workspace.yaml: set allowBuilds to true or false for ${this.pendingAllowBuilds.join(", ")}`,
110
+ );
111
+ }
112
+ // pob just modified package.json: the lockfile must be allowed to update,
113
+ // in particular in the automatic update GitHub Actions workflow (CI
114
+ // defaults to frozen lockfile)
115
+ this.spawnSync("pnpm", ["install", "--no-frozen-lockfile"], {});
95
116
  this.spawnSync("pnpm", ["dedupe"], {});
96
117
 
97
118
  this.fs.delete("package-lock.json");
@@ -1,3 +1,4 @@
1
+ // Generated by pob: do not edit, it is overwritten on each pob run.
1
2
  {
2
3
  // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
3
4
  // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
@@ -1,3 +1,4 @@
1
+ // Generated by pob: do not edit, it is overwritten on each pob run.
1
2
  {
2
3
  <% if (yarn) { -%>
3
4
  <% if (pnp) { -%>
@@ -1,3 +1,4 @@
1
+ # Generated by pob: do not edit, it is overwritten on each pob run.
1
2
  /unplugged
2
3
  /install-state.gz
3
4
  <% if (disableYarnGitCache) { -%>
@@ -1,5 +1,6 @@
1
1
  import Generator from "yeoman-generator";
2
2
  import inMonorepo from "../../../utils/inMonorepo.js";
3
+ import { readJSON5 } from "../../../utils/json5.js";
3
4
  import removeLegacyGeneratedDocs from "../../../utils/legacyGeneratedDocs.js";
4
5
  import * as packageUtils from "../../../utils/package.js";
5
6
  import { copyAndFormatTpl } from "../../../utils/writeAndFormat.js";
@@ -54,7 +55,8 @@ export default class LibDocGenerator extends Generator {
54
55
  : packageUtils.hasReact(pkg);
55
56
 
56
57
  if (inMonorepo && inMonorepo.root) {
57
- const existingConfig = this.fs.readJSON(
58
+ const existingConfig = readJSON5(
59
+ this.fs,
58
60
  this.destinationPath("tsconfig.doc.json"),
59
61
  {
60
62
  typedocOptions: {},
@@ -1,3 +1,4 @@
1
+ // Generated by pob: do not edit, it is overwritten on each pob run.
1
2
  {
2
3
  "extends": "./tsconfig.json",
3
4
 
@@ -1,3 +1,4 @@
1
+ // Generated by pob: do not edit, it is overwritten on each pob run.
1
2
  {
2
3
  "files": [],
3
4
 
@@ -1,5 +1,5 @@
1
- /* eslint-disable regexp/match-any */
2
- /* eslint-disable regexp/no-super-linear-backtracking */
1
+ /* oxlint-disable regexp/match-any */
2
+ /* oxlint-disable regexp/no-super-linear-backtracking */
3
3
  import camelCase from "lodash.camelcase";
4
4
  import Generator from "yeoman-generator";
5
5
  import inMonorepo from "../../../utils/inMonorepo.js";
@@ -255,6 +255,25 @@ export default class PobMonorepoGenerator extends Generator {
255
255
 
256
256
  this.composeWith("pob:common:husky", {});
257
257
 
258
+ // the path of the package running the e2e tests. The prompt only asks
259
+ // whether there are e2e tests.
260
+ const e2eTesting = (() => {
261
+ const value = this.pobMonorepoConfig.e2eTesting;
262
+ if (!value || value === "false") return "";
263
+ if (value !== true && value !== "true") return value;
264
+ const e2eLocations = this.packageLocations.filter(
265
+ (location, index) =>
266
+ this.packageConfigs[index]?.app?.e2e ||
267
+ this.packageConfigs[index]?.lib?.e2e,
268
+ );
269
+ if (e2eLocations.length > 1) {
270
+ console.warn(
271
+ `monorepo.e2eTesting: several packages have e2e tests (${e2eLocations.join(", ")}), set the path of the one to run in ci in .yo-rc.json`,
272
+ );
273
+ }
274
+ return e2eLocations.length === 1 ? e2eLocations[0] : ".";
275
+ })();
276
+
258
277
  const splitCIJobs = this.packageNames.length > 8;
259
278
 
260
279
  // the root has a build script when there are typescript definitions or rollup
@@ -271,7 +290,7 @@ export default class PobMonorepoGenerator extends Generator {
271
290
  runner: this.pobMonorepoConfig.testRunner,
272
291
  disableYarnGitCache: this.options.disableYarnGitCache,
273
292
  testing: this.pobMonorepoConfig.testing,
274
- e2eTesting: this.pobMonorepoConfig.e2eTesting,
293
+ e2eTesting,
275
294
  build: hasRootBuild,
276
295
  typescript: this.pobMonorepoConfig.typescript,
277
296
  documentation: !!this.pobMonorepoConfig.documentation,
@@ -284,11 +303,10 @@ export default class PobMonorepoGenerator extends Generator {
284
303
  splitCIJobs,
285
304
  });
286
305
 
306
+ const e2eTestingPath = e2eTesting === "." ? "" : `/${e2eTesting}`;
287
307
  const rootIgnorePaths = [
288
- this.pobMonorepoConfig.e2eTesting &&
289
- `${this.pobMonorepoConfig.e2eTesting === "." || this.pobMonorepoConfig.e2eTesting === true ? "" : `/${this.pobMonorepoConfig.e2eTesting}`}/playwright-report/`,
290
- this.pobMonorepoConfig.e2eTesting &&
291
- `${this.pobMonorepoConfig.e2eTesting === "." || this.pobMonorepoConfig.e2eTesting === true ? "" : `/${this.pobMonorepoConfig.e2eTesting}`}/test-results/`,
308
+ e2eTesting && `${e2eTestingPath}/playwright-report/`,
309
+ e2eTesting && `${e2eTestingPath}/test-results/`,
292
310
  ].filter(Boolean);
293
311
 
294
312
  if (hasTamagui(this.packages, this.packageConfigs)) {
@@ -310,6 +328,9 @@ export default class PobMonorepoGenerator extends Generator {
310
328
  packageManager: this.options.packageManager,
311
329
  yarnNodeLinker: this.options.yarnNodeLinker,
312
330
  appTypes: JSON.stringify(getAppTypes(this.packageConfigs)),
331
+ workspacesHaveReact: this.packages.some((workspacePkg) =>
332
+ packageUtils.hasReact(workspacePkg),
333
+ ),
313
334
  ignorePaths: [
314
335
  ...gitignorePaths.map((path) => `/${path}`),
315
336
  hasDist(this.packages, this.packageConfigs) && "/dist",
@@ -1,3 +1,4 @@
1
+ // Generated by pob: do not edit, it is overwritten on each pob run.
1
2
  <% configLocations.forEach((location, index) => { -%>
2
3
  import configs<%= index + 1 %> from './<%= location %>';
3
4
  <% }) -%>
@@ -67,6 +67,24 @@ export default class MonorepoTypescriptGenerator extends Generator {
67
67
  });
68
68
  }
69
69
 
70
+ // non-published typescript run in place is not part of any package's
71
+ // emitting tsconfig, so it needs a dedicated noEmit project for type-aware
72
+ // linting and tsc -b: root config files (e.g. vitest.config.ts), a root
73
+ // scripts/ dir, and each package's scripts/ dir (matched via the workspace
74
+ // globs so all packages are covered)
75
+ hasTools() {
76
+ return (
77
+ hasRootConfigFiles(this.destinationPath()) ||
78
+ existsSync(this.destinationPath("scripts"))
79
+ );
80
+ }
81
+
82
+ // tsc fails on a tsconfig.json without files nor references, like the one of
83
+ // a new monorepo without packages
84
+ hasTypescriptProjects() {
85
+ return JSON.parse(this.options.packagePaths).length > 0 || this.hasTools();
86
+ }
87
+
70
88
  writing() {
71
89
  if (this.fs.exists("flow-typed")) this.fs.delete("flow-typed");
72
90
  if (this.fs.exists(this.destinationPath(".flowconfig"))) {
@@ -91,7 +109,7 @@ export default class MonorepoTypescriptGenerator extends Generator {
91
109
  );
92
110
 
93
111
  if (this.options.enable) {
94
- packageUtils.addScripts(pkg, {
112
+ packageUtils.addOrRemoveScripts(pkg, this.hasTypescriptProjects(), {
95
113
  tsc: "tsc -b",
96
114
  });
97
115
  packageUtils.addOrRemoveScripts(
@@ -143,7 +161,7 @@ export default class MonorepoTypescriptGenerator extends Generator {
143
161
  this.fs.delete(this.destinationPath("tsconfig.root-configs.json"));
144
162
  this.fs.delete(tsconfigTestPath);
145
163
 
146
- if (!this.options.enable) {
164
+ if (!this.options.enable || !this.hasTypescriptProjects()) {
147
165
  this.fs.delete(tsconfigPath);
148
166
  this.fs.delete(tsconfigCheckPath);
149
167
  this.fs.delete(tsconfigBuildPath);
@@ -152,15 +170,8 @@ export default class MonorepoTypescriptGenerator extends Generator {
152
170
  const packagePaths = JSON.parse(this.options.packagePaths);
153
171
  const pkg = this.fs.readJSON(this.destinationPath("package.json"));
154
172
 
155
- // non-published typescript run in place is not part of any package's
156
- // emitting tsconfig, so it needs a dedicated noEmit project for type-aware
157
- // linting and tsc -b: root config files (e.g. vitest.config.ts), a root
158
- // scripts/ dir, and each package's scripts/ dir (matched via the
159
- // workspace globs so all packages are covered)
160
- const hasRootConfigs = hasRootConfigFiles(this.destinationPath());
161
- const hasRootScripts = existsSync(this.destinationPath("scripts"));
162
173
  const workspaceGlobs = pkg.workspaces || [];
163
- const hasTools = hasRootConfigs || hasRootScripts;
174
+ const hasTools = this.hasTools();
164
175
  if (hasTools) {
165
176
  await copyAndFormatTpl(
166
177
  this.fs,
@@ -1,3 +1,4 @@
1
+ // Generated by pob: do not edit, it is overwritten on each pob run.
1
2
  {
2
3
  "files": [],
3
4
  "references": [
@@ -1,3 +1,4 @@
1
+ // Generated by pob: do not edit, it is overwritten on each pob run.
1
2
  {
2
3
  "extends": [
3
4
  "@pob/root/tsconfigs/strict.json",
@@ -86,7 +86,7 @@ export default class MonorepoWorkspacesGenerator extends Generator {
86
86
  pkg.engines.npm = ">= 6.4.0";
87
87
  } else if (this.options.packageManager === "pnpm") {
88
88
  if (!pkg.engines) pkg.engines = {};
89
- pkg.engines.pnpm = ">= 11.0.0";
89
+ pkg.engines.pnpm = ">= 12.0.0";
90
90
  delete pkg.engines.yarn;
91
91
  } else if (pkg.engines) {
92
92
  delete pkg.engines.yarn;
@@ -123,8 +123,16 @@ export default class MonorepoWorkspacesGenerator extends Generator {
123
123
  if (this.options.isAppProject) {
124
124
  packageUtils.addOrRemoveScripts(pkg, withBundler, {
125
125
  build: workspacesRunTopological(packageManager, "build"),
126
- watch: workspacesRunExcluding(packageManager, "watch", "*-example"),
127
126
  });
127
+ // rollup apps have a start script, not a watch one
128
+ packageUtils.addOrRemoveScripts(
129
+ pkg,
130
+ withBundler &&
131
+ this.packages.some((packagePkg) => packagePkg.scripts?.watch),
132
+ {
133
+ watch: workspacesRunExcluding(packageManager, "watch", "*-example"),
134
+ },
135
+ );
128
136
  }
129
137
 
130
138
  // packageUtils.addOrRemoveScripts(pkg, withTypescript, {
@@ -157,17 +165,17 @@ export default class MonorepoWorkspacesGenerator extends Generator {
157
165
 
158
166
  if (this.fs.exists(readmePath)) {
159
167
  const readmeFullContent = this.fs.read(readmePath);
160
- // eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/match-any
168
+ // oxlint-disable-next-line regexp/no-super-linear-backtracking, regexp/match-any
161
169
  content = readmeFullContent.match(/^<h1 align="center"[^#*]+([^]+)$/);
162
170
  if (!content) {
163
- // eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/match-any
171
+ // oxlint-disable-next-line regexp/no-super-linear-backtracking, regexp/match-any
164
172
  content = readmeFullContent.match(/^<h3 align="center"[^#*]+([^]+)$/);
165
173
  }
166
174
  if (!content) {
167
- // eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/match-any
175
+ // oxlint-disable-next-line regexp/no-super-linear-backtracking, regexp/match-any
168
176
  content = readmeFullContent.match(/^<h3[^#*]+([^]+)$/);
169
177
  }
170
- // eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/match-any
178
+ // oxlint-disable-next-line regexp/no-super-linear-backtracking, regexp/match-any
171
179
  if (!content) content = readmeFullContent.match(/^#[^#*]+([^]+)$/);
172
180
  content = content ? content[1].trim() : readmeFullContent;
173
181
  }
@@ -212,8 +220,8 @@ export default class MonorepoWorkspacesGenerator extends Generator {
212
220
 
213
221
  switch (this.options.packageManager) {
214
222
  case "npm":
215
- this.spawnCommandSync("npm", ["install"]);
216
- this.spawnCommandSync("npm", ["run", "preversion"]);
223
+ this.spawnSync("npm", ["install"]);
224
+ this.spawnSync("npm", ["run", "preversion"]);
217
225
  break;
218
226
  case "pnpm":
219
227
  // see CorePnpmGenerator
@@ -300,7 +300,7 @@ export default class PobBaseGenerator extends Generator {
300
300
 
301
301
  switch (this.getPackageManager()) {
302
302
  case "npm":
303
- this.spawnCommandSync("npm", ["install"]);
303
+ this.spawnSync("npm", ["install"]);
304
304
  break;
305
305
  case "pnpm":
306
306
  case "yarn":
package/lib/pob.js CHANGED
@@ -47,11 +47,14 @@ import MonorepoTypescriptGenerator from "./generators/monorepo/typescript/Monore
47
47
  import MonorepoWorkspacesGenerator from "./generators/monorepo/workspaces/MonorepoWorkspacesGenerator.js";
48
48
  import PobBaseGenerator from "./generators/pob/PobBaseGenerator.js";
49
49
  import { __dirname } from "./pob-dirname.cjs";
50
+ import { getNewPackagePath } from "./utils/workspaceUtils.js";
50
51
 
51
52
  const printUsage = () => {
52
53
  console.error("Usage: pob [monorepo] [lib|app|init]");
53
54
  console.error(" pob [monorepo] update [--force]");
54
- console.error(" pob add <packageName>");
55
+ console.error(
56
+ " pob add <packageName> [--app <appType> [--testing] [--e2e]]",
57
+ );
55
58
  };
56
59
 
57
60
  const readJson = (filepath) => {
@@ -279,21 +282,55 @@ if (action === "add") {
279
282
  printUsage();
280
283
  process.exit(1);
281
284
  }
282
- const packagesPath = packageName.startsWith("@")
283
- ? packageName
284
- : projectPkg.workspaces[0].replace(/\/\*$/, "");
285
+ const packagePath = getNewPackagePath(projectPkg.workspaces, packageName);
286
+
287
+ if (existsSync(packagePath)) {
288
+ console.error(`${packagePath} already exists`);
289
+ process.exit(1);
290
+ }
291
+
292
+ // with --app, the package is generated without any prompt
293
+ const appType = argv.app;
294
+ const pobConfig = appType
295
+ ? {
296
+ pob: {
297
+ project: { type: "app" },
298
+ app: {
299
+ type: appType,
300
+ testing: !!argv.testing,
301
+ e2e: !!argv.e2e,
302
+ },
303
+ },
304
+ }
305
+ : {};
285
306
 
286
- fs.mkdirSync(`${packagesPath}/${packageName}`, { recursive: true });
287
- writeFileSync(`${packagesPath}/${packageName}/.yo-rc.json`, "{}");
307
+ fs.mkdirSync(packagePath, { recursive: true });
288
308
  writeFileSync(
289
- `${packagesPath}/${packageName}/package.json`,
290
- JSON.stringify({ name: packageName, version: "1.0.0-pre" }, null, 2),
309
+ `${packagePath}/.yo-rc.json`,
310
+ JSON.stringify(pobConfig, null, 2),
311
+ );
312
+ writeFileSync(
313
+ `${packagePath}/package.json`,
314
+ JSON.stringify(
315
+ {
316
+ name: packageName,
317
+ version: "1.0.0-pre",
318
+ ...(appType && { private: true }),
319
+ },
320
+ null,
321
+ 2,
322
+ ),
291
323
  );
292
324
  console.log("> Creating new Package");
293
- spawnSync(process.argv[0], [process.argv[1]], {
294
- cwd: `${packagesPath}/${packageName}`,
295
- stdio: "inherit",
296
- });
325
+ // --force: the package is new, its package.json is expected to change
326
+ spawnSync(
327
+ process.argv[0],
328
+ [process.argv[1], ...(appType ? ["update"] : []), "--force"],
329
+ {
330
+ cwd: packagePath,
331
+ stdio: "inherit",
332
+ },
333
+ );
297
334
 
298
335
  console.log("> Updating monorepo");
299
336
  spawnSync(process.argv[0], [process.argv[1], "update"], {
package/lib/utils/git.js CHANGED
@@ -1,5 +1,39 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
 
3
+ /**
4
+ * Repository name for a package name: without its scope, nor the "-monorepo"
5
+ * suffix of monorepo roots.
6
+ */
7
+ export const getRepoName = (pkgName) =>
8
+ pkgName.replace(/^@[^/]+\//, "").replace(/-monorepo$/, "");
9
+
10
+ // characters allowed by GitHub in account and repository names
11
+ const validGitNameRegex = /^[\w.-]+$/;
12
+
13
+ /**
14
+ * Parses a git remote url, or the "repository" field of a package.json.
15
+ * Returns undefined when the url cannot come from a real remote.
16
+ */
17
+ export const parseRepositoryUrl = (repository) => {
18
+ const url = typeof repository === "string" ? repository : repository?.url;
19
+ if (!url || typeof url !== "string") return undefined;
20
+
21
+ const match = url.match(
22
+ /^(?:git\+)?(?:git@|ssh:\/\/git@|https?:\/\/)([^./:]+)(?:\.[a-z]+)?[/:]([^/]+)\/([^/]+?)(?:\.git)?\/?$/,
23
+ );
24
+ if (!match) return undefined;
25
+
26
+ const [, gitHost, gitAccount, repoName] = match;
27
+ if (
28
+ !validGitNameRegex.test(gitAccount) ||
29
+ !validGitNameRegex.test(repoName)
30
+ ) {
31
+ return undefined;
32
+ }
33
+
34
+ return { gitHost, gitAccount, repoName };
35
+ };
36
+
3
37
  export const isTrackedInGit = (cwd, path) => {
4
38
  try {
5
39
  return (
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getRepoName, parseRepositoryUrl } from "./git.js";
3
+
4
+ describe("getRepoName", () => {
5
+ it("removes the scope and the monorepo suffix", () => {
6
+ expect(getRepoName("@chapplications/stories-monorepo")).toBe("stories");
7
+ });
8
+
9
+ it("removes the monorepo suffix", () => {
10
+ expect(getRepoName("pob-monorepo")).toBe("pob");
11
+ });
12
+
13
+ it("keeps a plain name", () => {
14
+ expect(getRepoName("foo")).toBe("foo");
15
+ });
16
+ });
17
+
18
+ describe("parseRepositoryUrl", () => {
19
+ it("parses an https url", () => {
20
+ expect(
21
+ parseRepositoryUrl("https://github.com/christophehurpeau/pob.git"),
22
+ ).toEqual({
23
+ gitHost: "github",
24
+ gitAccount: "christophehurpeau",
25
+ repoName: "pob",
26
+ });
27
+ });
28
+
29
+ it("parses an ssh url", () => {
30
+ expect(
31
+ parseRepositoryUrl("git@github.com:christophehurpeau/pob.git"),
32
+ ).toEqual({
33
+ gitHost: "github",
34
+ gitAccount: "christophehurpeau",
35
+ repoName: "pob",
36
+ });
37
+ });
38
+
39
+ it("parses a repository object and a name with a dot", () => {
40
+ expect(
41
+ parseRepositoryUrl({
42
+ type: "git",
43
+ url: "git+https://github.com/christophehurpeau/foo.js",
44
+ }),
45
+ ).toEqual({
46
+ gitHost: "github",
47
+ gitAccount: "christophehurpeau",
48
+ repoName: "foo.js",
49
+ });
50
+ });
51
+
52
+ it("rejects a url built from a scoped package name", () => {
53
+ expect(
54
+ parseRepositoryUrl(
55
+ "https://github.com/christophehurpeau/@chapplications/stories-monorepo.git",
56
+ ),
57
+ ).toBeUndefined();
58
+ });
59
+
60
+ it("rejects a url with an invalid repository name", () => {
61
+ expect(
62
+ parseRepositoryUrl(
63
+ "https://github.com/christophehurpeau/@chapplications",
64
+ ),
65
+ ).toBeUndefined();
66
+ });
67
+
68
+ it("returns undefined without url", () => {
69
+ expect(parseRepositoryUrl(undefined)).toBeUndefined();
70
+ });
71
+ });
@@ -8,8 +8,18 @@ export const getWorkspaceName = (workspace) => {
8
8
  return path.basename(workspace.location) || "unnamed-workspace";
9
9
  };
10
10
 
11
+ /**
12
+ * Path of a package created by "pob add": in the first workspaces directory,
13
+ * named without the scope.
14
+ */
15
+ export const getNewPackagePath = (workspaces, packageName) =>
16
+ `${workspaces[0].replace(/\/\*$/, "")}/${packageName.replace(/^@[^/]+\//, "")}`;
17
+
11
18
  export const discoverWorkspaces = async (rootPath) => {
12
19
  const rootPackageJSONPath = path.join(rootPath, "package.json");
20
+ if (!fs.existsSync(rootPackageJSONPath)) {
21
+ return [];
22
+ }
13
23
  const rootPkg = JSON.parse(fs.readFileSync(rootPackageJSONPath));
14
24
 
15
25
  let workspaceGlobs = [];
@@ -6,8 +6,23 @@ import {
6
6
  buildDependenciesMaps,
7
7
  buildTopologicalOrderBatches,
8
8
  discoverWorkspaces,
9
+ getNewPackagePath,
9
10
  } from "./workspaceUtils.js";
10
11
 
12
+ describe("getNewPackagePath", () => {
13
+ it("creates a scoped package in the workspaces directory", () => {
14
+ expect(getNewPackagePath(["packages/*"], "@scope/foo")).toBe(
15
+ "packages/foo",
16
+ );
17
+ });
18
+
19
+ it("creates an unscoped package in the workspaces directory", () => {
20
+ expect(getNewPackagePath(["packages/*", "@pob/*"], "foo")).toBe(
21
+ "packages/foo",
22
+ );
23
+ });
24
+ });
25
+
11
26
  async function prepareMonorepo(tmpDir) {
12
27
  await fs.mkdir(path.join(tmpDir, "packages"), { recursive: true });
13
28
  const rootPkg = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pob",
3
- "version": "38.10.0",
3
+ "version": "38.12.0",
4
4
  "description": "Pile of bones, library generator with git/babel/typescript/typedoc/readme/vitest",
5
5
  "keywords": [
6
6
  "skeleton"
@@ -16,6 +16,9 @@
16
16
  "url": "https://github.com/christophehurpeau/pob.git",
17
17
  "directory": "packages/pob"
18
18
  },
19
+ "bin": {
20
+ "pob": "./lib/pob.js"
21
+ },
19
22
  "files": [
20
23
  "lib",
21
24
  "bin"
@@ -28,6 +31,10 @@
28
31
  "./package.json": "./package.json"
29
32
  },
30
33
  "dependencies": {
34
+ "@pob/eslint-config": "67.11.0",
35
+ "@pob/eslint-config-typescript-react": "67.11.0",
36
+ "@pob/sort-object": "11.2.0",
37
+ "@pob/sort-pkg": "13.2.0",
31
38
  "@types/inquirer": "9.0.10",
32
39
  "@yeoman/adapter": "4.0.2",
33
40
  "eslint": "10.8.1",
@@ -43,22 +50,18 @@
43
50
  "minimist": "1.2.8",
44
51
  "oxfmt": "0.54.0",
45
52
  "parse-author": "2.0.0",
53
+ "pob-dependencies": "27.11.0",
46
54
  "semver": "7.8.5",
47
55
  "typescript": "npm:@typescript/typescript6@6.0.2",
48
56
  "validate-npm-package-name": "^8.0.0",
49
57
  "yeoman-environment": "6.1.0",
50
- "yeoman-generator": "8.2.2",
51
- "@pob/eslint-config-typescript-react": "67.9.0",
52
- "@pob/sort-object": "11.2.0",
53
- "@pob/sort-pkg": "13.2.0",
54
- "pob-dependencies": "27.9.0",
55
- "@pob/eslint-config": "67.9.0"
58
+ "yeoman-generator": "8.2.2"
56
59
  },
57
60
  "devDependencies": {
58
- "@types/node": "24.13.3",
61
+ "@pob/root": "27.12.0",
62
+ "@types/node": "24.19.1",
59
63
  "@typescript/native": "npm:typescript@7.0.2",
60
- "@yeoman/types": "1.11.1",
61
- "@pob/root": "27.10.0"
64
+ "@yeoman/types": "1.11.1"
62
65
  },
63
66
  "engines": {
64
67
  "node": ">=22.18.0"
@@ -73,8 +76,5 @@
73
76
  "lint:eslint": "pnpm --dir '../..' exec eslint --quiet 'packages/pob'",
74
77
  "lint:oxlint": "pnpm --dir '../..' exec oxlint --quiet 'packages/pob'",
75
78
  "tsc": "tsc"
76
- },
77
- "bin": {
78
- "pob": "./lib/pob.js"
79
79
  }
80
80
  }
@@ -1,5 +0,0 @@
1
- <%- imports.map(v => `${v};\n`).join('') -%>
2
-
3
- export default [
4
- <%- flatCascade.map(v => ` ${v},\n`).join('') -%>
5
- ];