pob 40.3.2 → 40.4.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 (31) hide show
  1. package/lib/generators/app/PobAppGenerator.js +24 -4
  2. package/lib/generators/common/format-lint/CommonLintGenerator.js +14 -2
  3. package/lib/generators/common/husky/CommonHuskyGenerator.js +3 -1
  4. package/lib/generators/common/testing/CommonTestingGenerator.js +10 -2
  5. package/lib/generators/common/transpiler/CommonTranspilerGenerator.js +11 -9
  6. package/lib/generators/common/typescript/CommonTypescriptGenerator.js +110 -38
  7. package/lib/generators/core/bun/CoreBunGenerator.js +1 -1
  8. package/lib/generators/core/ci/CoreCIGenerator.js +4 -1
  9. package/lib/generators/core/git/CoreGitGenerator.js +2 -0
  10. package/lib/generators/core/git/generators/github/CoreGitGithubGenerator.js +1 -0
  11. package/lib/generators/core/package/CorePackageGenerator.js +12 -3
  12. package/lib/generators/core/pnpm/CorePnpmGenerator.js +8 -2
  13. package/lib/generators/core/vscode/CoreVSCodeGenerator.js +9 -0
  14. package/lib/generators/core/vscode/templates/extensions.json.ejs +2 -1
  15. package/lib/generators/core/vscode/templates/settings.json.ejs +5 -0
  16. package/lib/generators/core/yarn/CoreYarnGenerator.js +6 -2
  17. package/lib/generators/lib/PobLibGenerator.js +15 -13
  18. package/lib/generators/monorepo/PobMonorepoGenerator.js +44 -6
  19. package/lib/generators/monorepo/typescript/MonorepoTypescriptGenerator.js +13 -1
  20. package/lib/generators/monorepo/typescript/templates/tsconfig.tools.json.ejs +7 -0
  21. package/lib/generators/monorepo/workspaces/MonorepoWorkspacesGenerator.js +7 -2
  22. package/lib/generators/pob/PobBaseGenerator.js +4 -2
  23. package/lib/pob.js +75 -5
  24. package/lib/utils/alouette.js +18 -0
  25. package/lib/utils/appTsconfigTarget.js +60 -0
  26. package/lib/utils/appTsconfigTarget.test.js +169 -0
  27. package/lib/utils/nodeApps.js +8 -0
  28. package/lib/utils/packageManagerWorkspacesUtils.js +36 -0
  29. package/lib/utils/rollupConfigDependencies.js +17 -0
  30. package/lib/utils/rollupConfigDependencies.test.js +48 -0
  31. package/package.json +5 -5
@@ -2,13 +2,18 @@ import { execSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import { platform } from "node:process";
4
4
  import Generator from "yeoman-generator";
5
+ import { isAlouetteApp } from "../../utils/alouette.js";
5
6
  import removeLegacyGeneratedDocs from "../../utils/legacyGeneratedDocs.js";
7
+ import { isNodeApp } from "../../utils/nodeApps.js";
6
8
  import * as packageUtils from "../../utils/package.js";
7
9
  import { packageManagerRun } from "../../utils/packageManagerUtils.js";
8
10
  import {
9
11
  workspacesRun,
12
+ workspacesRunExcluding,
13
+ workspacesRunOnly,
10
14
  workspacesRunSequential,
11
15
  } from "../../utils/packageManagerWorkspacesUtils.js";
16
+ import { getRollupConfigDependencies } from "../../utils/rollupConfigDependencies.js";
12
17
  import {
13
18
  detectStorybook,
14
19
  detectStorybookReactNative,
@@ -386,6 +391,9 @@ export default class PobMonorepoGenerator extends Generator {
386
391
  typescript: this.pobMonorepoConfig.typescript,
387
392
  testing: this.pobMonorepoConfig.testing,
388
393
  testRunner: this.pobMonorepoConfig.testRunner,
394
+ tailwind: this.packageConfigs.some((config, index) =>
395
+ isAlouetteApp(config, this.packages[index]),
396
+ ),
389
397
  packageNames: JSON.stringify(packageNames),
390
398
  packageLocations: JSON.stringify(this.packageLocations),
391
399
  });
@@ -425,6 +433,17 @@ export default class PobMonorepoGenerator extends Generator {
425
433
  isAppProject: this.options.isAppProject,
426
434
  packageNames: JSON.stringify(packageNames),
427
435
  packagePaths: JSON.stringify(packagePaths),
436
+ // mirrors the includeScripts option given by the app generator
437
+ packagePathsWithScriptsIncluded: JSON.stringify(
438
+ this.packageLocations.filter(
439
+ (location, index) =>
440
+ this.pobMonorepoConfig.typescript !== "check-only" &&
441
+ this.packages[index].pob?.typescript !== "check-only" &&
442
+ isNodeApp(this.packageConfigs[index]) &&
443
+ packagePaths.includes(location) &&
444
+ fs.existsSync(`${location}/scripts`),
445
+ ),
446
+ ),
428
447
  testRunner: this.pobMonorepoConfig.testRunner,
429
448
  onlyLatestLTS: this.options.onlyLatestLTS,
430
449
  packageManager: this.options.packageManager,
@@ -432,6 +451,8 @@ export default class PobMonorepoGenerator extends Generator {
432
451
 
433
452
  this.fs.writeJSON(this.destinationPath("package.json"), pkg);
434
453
 
454
+ if (this.options.check) return;
455
+
435
456
  if (platform !== "win32") {
436
457
  execSync(
437
458
  `rm -Rf ${["lib-*", "coverage", "generated-docs"].filter(Boolean).join(" ")}`,
@@ -453,6 +474,7 @@ export default class PobMonorepoGenerator extends Generator {
453
474
  });
454
475
 
455
476
  const rollupConfigs = [];
477
+ const rollupConfigContents = [];
456
478
  this.packageLocations.forEach((location) => {
457
479
  const rollupPath = `${location}/rollup.config.mjs`;
458
480
  const rollupConfig = this.fs.read(this.destinationPath(rollupPath), {
@@ -460,9 +482,21 @@ export default class PobMonorepoGenerator extends Generator {
460
482
  });
461
483
  if (rollupConfig) {
462
484
  rollupConfigs.push(rollupPath);
485
+ rollupConfigContents.push(rollupConfig);
463
486
  }
464
487
  });
465
488
 
489
+ // built before the root rollup config loads them, and kept out of
490
+ // clean:build which runs in between
491
+ const rollupConfigDependencies = getRollupConfigDependencies(
492
+ this.packages,
493
+ rollupConfigContents,
494
+ );
495
+ const buildRollupConfigDependencies =
496
+ rollupConfigDependencies.length > 0
497
+ ? `${workspacesRunOnly(this.options.packageManager, "build", ...rollupConfigDependencies)} && `
498
+ : "";
499
+
466
500
  if (rollupConfigs.length > 0) {
467
501
  await copyAndFormatTpl(
468
502
  this.fs,
@@ -476,12 +510,16 @@ export default class PobMonorepoGenerator extends Generator {
476
510
  this.fs.delete("rollup.config.mjs");
477
511
  }
478
512
  packageUtils.addOrRemoveScripts(pkg, rollupConfigs.length > 0, {
479
- "clean:build": workspacesRun(
480
- this.options.packageManager,
481
- "clean:build",
482
- ),
483
- build: `${packageManagerRun(this.options.packageManager, "clean:build")} && rollup --config rollup.config.mjs`,
484
- watch: `${packageManagerRun(this.options.packageManager, "clean:build")} && rollup --config rollup.config.mjs --watch`,
513
+ "clean:build":
514
+ rollupConfigDependencies.length > 0
515
+ ? workspacesRunExcluding(
516
+ this.options.packageManager,
517
+ "clean:build",
518
+ ...rollupConfigDependencies,
519
+ )
520
+ : workspacesRun(this.options.packageManager, "clean:build"),
521
+ build: `${buildRollupConfigDependencies}${packageManagerRun(this.options.packageManager, "clean:build")} && rollup --config rollup.config.mjs`,
522
+ watch: `${buildRollupConfigDependencies}${packageManagerRun(this.options.packageManager, "clean:build")} && rollup --config rollup.config.mjs --watch`,
485
523
  });
486
524
 
487
525
  const hasExpoApp = this.packageConfigs.some(
@@ -57,6 +57,14 @@ export default class MonorepoTypescriptGenerator extends Generator {
57
57
  required: true,
58
58
  });
59
59
 
60
+ this.option("packagePathsWithScriptsIncluded", {
61
+ type: String,
62
+ required: false,
63
+ default: "[]",
64
+ description:
65
+ "packages whose tsconfig.json includes their scripts (node apps)",
66
+ });
67
+
60
68
  this.option("testRunner", {
61
69
  type: String,
62
70
  required: false,
@@ -85,7 +93,8 @@ export default class MonorepoTypescriptGenerator extends Generator {
85
93
  // emitting tsconfig, so it needs a dedicated noEmit project for type-aware
86
94
  // linting and tsc -b: root config files (e.g. vitest.config.ts), a root
87
95
  // scripts/ dir, and each package's scripts/ dir (matched via the workspace
88
- // globs so all packages are covered)
96
+ // globs so all packages are covered, except node apps which include them in
97
+ // their own tsconfig.json)
89
98
  #hasTools() {
90
99
  return (
91
100
  hasRootConfigFiles(this.destinationPath()) ||
@@ -199,6 +208,9 @@ export default class MonorepoTypescriptGenerator extends Generator {
199
208
  {
200
209
  nodeVersion: latestLTS,
201
210
  workspaceGlobs,
211
+ packagePathsWithScriptsIncluded: JSON.parse(
212
+ this.options.packagePathsWithScriptsIncluded,
213
+ ),
202
214
  },
203
215
  );
204
216
  } else {
@@ -21,6 +21,13 @@
21
21
  "<%= workspaceGlob %>/e2e/**/*.cts"<%= index === workspaceGlobs.length - 1 ? '' : ',' %>
22
22
  <% }) -%>
23
23
  ],
24
+ <% if (packagePathsWithScriptsIncluded.length > 0) { -%>
25
+ "exclude": [
26
+ <% packagePathsWithScriptsIncluded.forEach((packagePath, index) => { -%>
27
+ "<%= packagePath %>/scripts/**"<%= index === packagePathsWithScriptsIncluded.length - 1 ? '' : ',' %>
28
+ <% }) -%>
29
+ ],
30
+ <% } -%>
24
31
  "compilerOptions": {
25
32
  "noEmit": true,
26
33
  "tsBuildInfoFile": "node_modules/.cache/tsc/tsbuildinfo-tools",
@@ -199,21 +199,26 @@ export default class MonorepoWorkspacesGenerator extends Generator {
199
199
  return;
200
200
  }
201
201
  console.log(`=> update ${packagePath}`);
202
- spawnSync(
202
+ const { status } = spawnSync(
203
203
  process.argv[0],
204
204
  [
205
205
  process.argv[1],
206
206
  "update",
207
207
  "from-pob",
208
- this.options.force ? "--force" : undefined,
208
+ this.options.check && "--check",
209
+ !this.options.check && this.options.force && "--force",
209
210
  ].filter(Boolean),
210
211
  {
211
212
  cwd: packagePath,
212
213
  stdio: "inherit",
213
214
  },
214
215
  );
216
+ // with --check, a package exits with 1 when a file would change
217
+ if (this.options.check && status !== 0) process.exitCode = 1;
215
218
  });
216
219
 
220
+ if (this.options.check) return;
221
+
217
222
  switch (this.options.packageManager) {
218
223
  case "npm":
219
224
  this.spawnSync("npm", ["install"]);
@@ -45,7 +45,9 @@ export default class PobBaseGenerator extends Generator {
45
45
 
46
46
  async initializing() {
47
47
  // format package.json to ensure diff is correct
48
- await ensureJsonFileFormatted(this.destinationPath("package.json"));
48
+ if (!this.options.check) {
49
+ await ensureJsonFileFormatted(this.destinationPath("package.json"));
50
+ }
49
51
 
50
52
  if (this.options.monorepo) {
51
53
  this.isMonorepo = true;
@@ -296,7 +298,7 @@ export default class PobBaseGenerator extends Generator {
296
298
  }
297
299
 
298
300
  install() {
299
- if (this.options.fromPob) return;
301
+ if (this.options.fromPob || this.options.check) return;
300
302
 
301
303
  switch (this.#getPackageManager()) {
302
304
  case "npm":
package/lib/pob.js CHANGED
@@ -51,12 +51,42 @@ import { getNewPackagePath } from "./utils/workspaceUtils.js";
51
51
 
52
52
  const printUsage = () => {
53
53
  console.error("Usage: pob [monorepo] [lib|app|init]");
54
- console.error(" pob [monorepo] update [--force]");
54
+ console.error(" pob [monorepo] update [--force|--check]");
55
55
  console.error(
56
56
  " pob add <packageName> [--app <appType> [--testing] [--e2e]]",
57
57
  );
58
58
  };
59
59
 
60
+ const printHelp = () => {
61
+ console.log(`Usage: pob [monorepo] [lib|app|init]
62
+ pob [monorepo] update [from-pob] [--force|--check]
63
+ pob add <packageName> [--app <appType> [--testing] [--e2e]]
64
+ pob migrate-to-monorepo
65
+
66
+ Commands:
67
+ lib Generate a library
68
+ app Generate an app
69
+ init Initialize pob config, prompting for project type
70
+ update Regenerate files from the existing .yo-rc.json, without prompts
71
+ update from-pob Same as update, without running install or build
72
+ add <packageName> Add a new package to the monorepo workspaces
73
+ migrate-to-monorepo Move the current project into packages/<name> and create a monorepo root
74
+
75
+ monorepo Prefix to force monorepo mode (auto-detected from workspaces or lerna.json)
76
+
77
+ Options:
78
+ --force Overwrite every file without asking
79
+ --check With update (implied when no command is given): print
80
+ the diffs without writing anything or running install,
81
+ exit with code 1 when a file would change
82
+ --app <appType> With add: generate an app package without prompts
83
+ (alp, vite, vite-with-server, next.js, node, node-library,
84
+ untranspiled-library, alp-node, expo, other)
85
+ --testing With add --app: enable testing
86
+ --e2e With add --app: enable e2e testing
87
+ -h, --help Show this help`);
88
+ };
89
+
60
90
  const readJson = (filepath) => {
61
91
  try {
62
92
  return JSON.parse(readFileSync(filepath, "utf8"));
@@ -69,14 +99,39 @@ const readJson = (filepath) => {
69
99
  // console.log(pkg.version);
70
100
  // };
71
101
 
72
- const argv = minimist(process.argv.slice(2));
102
+ const argv = minimist(process.argv.slice(2), { alias: { h: "help" } });
103
+
104
+ if (argv.help || argv._[0] === "help") {
105
+ printHelp();
106
+ process.exit(0);
107
+ }
73
108
 
74
109
  if (argv.version) {
75
110
  // printVersion();
76
111
  process.exit(0);
77
112
  }
78
113
 
79
- const env = yeoman.createEnv();
114
+ const check = !!argv.check;
115
+
116
+ // dryRun: the conflicter prints the diffs and skips every write.
117
+ // sharedOptions.check: generators skip their side effects outside mem-fs.
118
+ const env = yeoman.createEnv(
119
+ check
120
+ ? { dryRun: true, skipInstall: true, sharedOptions: { check: true } }
121
+ : {},
122
+ );
123
+
124
+ // the conflicter clears its file statuses after each file: collect them as logged
125
+ const changedFiles = [];
126
+ if (check) {
127
+ for (const status of ["create", "conflict"]) {
128
+ const log = env.adapter.log[status];
129
+ env.adapter.log[status] = (filePath, ...args) => {
130
+ changedFiles.push(filePath);
131
+ return log.call(env.adapter.log, filePath, ...args);
132
+ };
133
+ }
134
+ }
80
135
 
81
136
  env.on("error", (err) => {
82
137
  console.error(err.stack || err.message || err);
@@ -266,7 +321,13 @@ env.registerStub(
266
321
  const projectPkg = readJson(path.resolve("./package.json"));
267
322
  const monorepoArg = argv._[0] === "lerna" || argv._[0] === "monorepo";
268
323
  let monorepo = monorepoArg || !!(projectPkg && projectPkg.workspaces);
269
- const action = monorepoArg ? argv._[1] : argv._[0];
324
+ const action = (monorepoArg ? argv._[1] : argv._[0]) ?? (check && "update");
325
+
326
+ if (check && action !== "update") {
327
+ console.error("--check can only be used with update");
328
+ printUsage();
329
+ process.exit(1);
330
+ }
270
331
 
271
332
  if (action === "add") {
272
333
  if (!projectPkg.workspaces) {
@@ -417,7 +478,8 @@ const options = {
417
478
  updateOnly,
418
479
  monorepo,
419
480
  fromPob,
420
- force: argv.force,
481
+ // force bypasses the conflicter diff
482
+ force: check ? false : argv.force,
421
483
  };
422
484
 
423
485
  try {
@@ -429,6 +491,14 @@ try {
429
491
  }
430
492
  }
431
493
 
494
+ if (check) {
495
+ if (changedFiles.length > 0) {
496
+ console.error(`pob --check: ${changedFiles.length} file(s) would change:`);
497
+ for (const file of changedFiles) console.error(` ${file}`);
498
+ process.exitCode = 1;
499
+ }
500
+ }
501
+
432
502
  // generator.on('error', (err) => {
433
503
  // console.error(err.stack || err.message || err);
434
504
  // process.exit(1);
@@ -0,0 +1,18 @@
1
+ // alouette styles components with tailwind (nativewind) and tailwind-variants
2
+
3
+ /** @param {Record<string, any>} pkg */
4
+ export const detectAlouette = (pkg) =>
5
+ [pkg.dependencies, pkg.devDependencies].some(
6
+ (dependencies) => dependencies?.alouette !== undefined,
7
+ );
8
+
9
+ /**
10
+ * Fallback on the dependencies for an app config missing the option.
11
+ *
12
+ * @param {Record<string, any> | undefined} pobConfig
13
+ * @param {Record<string, any>} pkg
14
+ */
15
+ export const isAlouetteApp = (pobConfig, pkg) =>
16
+ pobConfig?.project?.type === "app" &&
17
+ pobConfig.app?.type === "expo" &&
18
+ (pobConfig.app.alouette ?? detectAlouette(pkg));
@@ -0,0 +1,60 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { buildDependentsMaps, discoverWorkspaces } from "./workspaceUtils.js";
4
+
5
+ export const appsWithBrowser = ["alp", "next.js", "vite", "vite-with-server"];
6
+
7
+ const expoTarget = "@pob/root/tsconfigs/targets/expo.json";
8
+
9
+ // based on the app type from the pob config rather than on the package.json
10
+ // dependencies, which can change without pob being run again
11
+ export const getAppTsconfigTarget = ({ appType, dom, withBun }) => {
12
+ if (appType === "expo") return expoTarget;
13
+ if (dom) {
14
+ return appType === "vite" || appType === "vite-with-server"
15
+ ? "@pob/root/tsconfigs/targets/vite.json"
16
+ : "@pob/root/tsconfigs/targets/webpack.json";
17
+ }
18
+ if (withBun) return "@pob/root/tsconfigs/targets/bun.json";
19
+ return undefined;
20
+ };
21
+
22
+ const readPobConfig = (location) => {
23
+ try {
24
+ return JSON.parse(fs.readFileSync(path.join(location, ".yo-rc.json"))).pob;
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ };
29
+
30
+ /**
31
+ * Target of the apps directly depending on the package, for packages compiled
32
+ * by the bundler of these apps (untranspiled libraries). Expo wins as the
33
+ * widest target, otherwise the apps must agree on a single target.
34
+ */
35
+ export const getConsumerAppsTsconfigTarget = async ({
36
+ rootPath,
37
+ packageName,
38
+ withBun,
39
+ }) => {
40
+ const workspaces = await discoverWorkspaces(rootPath);
41
+ const workspace = workspaces.find((w) => w.name === packageName);
42
+ if (!workspace) return undefined;
43
+
44
+ const dependents = buildDependentsMaps(workspaces).get(workspace) || [];
45
+ const targets = new Set();
46
+ for (const [dependent] of dependents) {
47
+ const pobConfig = readPobConfig(path.join(rootPath, dependent.location));
48
+ if (pobConfig?.project?.type !== "app") continue;
49
+ const appType = pobConfig.app?.type;
50
+ const target = getAppTsconfigTarget({
51
+ appType,
52
+ dom: appsWithBrowser.includes(appType),
53
+ withBun,
54
+ });
55
+ if (target) targets.add(target);
56
+ }
57
+
58
+ if (targets.has(expoTarget)) return expoTarget;
59
+ return targets.size === 1 ? [...targets][0] : undefined;
60
+ };
@@ -0,0 +1,169 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import {
6
+ getAppTsconfigTarget,
7
+ getConsumerAppsTsconfigTarget,
8
+ } from "./appTsconfigTarget.js";
9
+
10
+ const targets = {
11
+ expo: "@pob/root/tsconfigs/targets/expo.json",
12
+ vite: "@pob/root/tsconfigs/targets/vite.json",
13
+ webpack: "@pob/root/tsconfigs/targets/webpack.json",
14
+ bun: "@pob/root/tsconfigs/targets/bun.json",
15
+ };
16
+
17
+ describe("getAppTsconfigTarget", () => {
18
+ it("returns expo for expo apps", () => {
19
+ expect(getAppTsconfigTarget({ appType: "expo", dom: false })).toBe(
20
+ targets.expo,
21
+ );
22
+ });
23
+
24
+ it("returns vite for vite apps", () => {
25
+ expect(getAppTsconfigTarget({ appType: "vite", dom: true })).toBe(
26
+ targets.vite,
27
+ );
28
+ expect(
29
+ getAppTsconfigTarget({ appType: "vite-with-server", dom: true }),
30
+ ).toBe(targets.vite);
31
+ });
32
+
33
+ it("returns webpack for other browser apps and dom libraries", () => {
34
+ expect(getAppTsconfigTarget({ appType: "alp", dom: true })).toBe(
35
+ targets.webpack,
36
+ );
37
+ expect(getAppTsconfigTarget({ appType: undefined, dom: true })).toBe(
38
+ targets.webpack,
39
+ );
40
+ });
41
+
42
+ it("returns bun without dom with bun", () => {
43
+ expect(
44
+ getAppTsconfigTarget({ appType: "node", dom: false, withBun: true }),
45
+ ).toBe(targets.bun);
46
+ });
47
+
48
+ it("returns undefined for node apps", () => {
49
+ expect(getAppTsconfigTarget({ appType: "node", dom: false })).toBe(
50
+ undefined,
51
+ );
52
+ });
53
+ });
54
+
55
+ async function writeWorkspace(rootPath, location, pkg, pobConfig) {
56
+ const dir = path.join(rootPath, location);
57
+ await fs.mkdir(dir, { recursive: true });
58
+ await fs.writeFile(path.join(dir, "package.json"), JSON.stringify(pkg));
59
+ if (pobConfig) {
60
+ await fs.writeFile(
61
+ path.join(dir, ".yo-rc.json"),
62
+ JSON.stringify({ pob: pobConfig }),
63
+ );
64
+ }
65
+ }
66
+
67
+ const app = (type) => ({ project: { type: "app" }, app: { type } });
68
+
69
+ /**
70
+ * Monorepo with an untranspiled library "@ex/lib" and the given consumers,
71
+ * each one an [appType, pobConfig] pair depending on the library.
72
+ */
73
+ async function prepareMonorepo(consumers) {
74
+ const rootPath = await fs.mkdtemp(path.join(os.tmpdir(), "pob-"));
75
+ await writeWorkspace(rootPath, ".", {
76
+ name: "root",
77
+ workspaces: ["packages/*"],
78
+ });
79
+ await writeWorkspace(
80
+ rootPath,
81
+ "packages/lib",
82
+ { name: "@ex/lib" },
83
+ app("untranspiled-library"),
84
+ );
85
+ for (const [name, pobConfig] of consumers) {
86
+ await writeWorkspace(
87
+ rootPath,
88
+ `packages/${name}`,
89
+ { name: `@ex/${name}`, dependencies: { "@ex/lib": "workspace:*" } },
90
+ pobConfig,
91
+ );
92
+ }
93
+ return rootPath;
94
+ }
95
+
96
+ const getLibTarget = async (consumers, { withBun = false } = {}) =>
97
+ getConsumerAppsTsconfigTarget({
98
+ rootPath: await prepareMonorepo(consumers),
99
+ packageName: "@ex/lib",
100
+ withBun,
101
+ });
102
+
103
+ describe("getConsumerAppsTsconfigTarget", () => {
104
+ it("uses the target of a consumer expo app", async () => {
105
+ expect(await getLibTarget([["mobile", app("expo")]])).toBe(targets.expo);
106
+ });
107
+
108
+ it("prefers expo when consumer apps disagree", async () => {
109
+ expect(
110
+ await getLibTarget([
111
+ ["mobile", app("expo")],
112
+ ["web", app("vite")],
113
+ ["api", app("node")],
114
+ ]),
115
+ ).toBe(targets.expo);
116
+ });
117
+
118
+ it("uses the target shared by all consumer apps", async () => {
119
+ expect(
120
+ await getLibTarget([
121
+ ["web", app("vite")],
122
+ ["api", app("node")],
123
+ ]),
124
+ ).toBe(targets.vite);
125
+ });
126
+
127
+ it("returns undefined when consumer apps disagree without expo", async () => {
128
+ expect(
129
+ await getLibTarget([
130
+ ["web", app("vite")],
131
+ ["legacy", app("alp")],
132
+ ]),
133
+ ).toBe(undefined);
134
+ });
135
+
136
+ it("returns undefined without consumer apps with a target", async () => {
137
+ expect(await getLibTarget([])).toBe(undefined);
138
+ expect(await getLibTarget([["api", app("node")]])).toBe(undefined);
139
+ expect(
140
+ await getLibTarget([["other-lib", { project: { type: "lib" } }]]),
141
+ ).toBe(undefined);
142
+ expect(await getLibTarget([["no-config", undefined]])).toBe(undefined);
143
+ });
144
+
145
+ it("uses bun for node consumer apps with bun", async () => {
146
+ expect(await getLibTarget([["api", app("node")]], { withBun: true })).toBe(
147
+ targets.bun,
148
+ );
149
+ });
150
+
151
+ it("ignores apps depending on the library through another library", async () => {
152
+ const rootPath = await prepareMonorepo([
153
+ ["other-lib", app("untranspiled-library")],
154
+ ]);
155
+ await writeWorkspace(
156
+ rootPath,
157
+ "packages/mobile",
158
+ { name: "@ex/mobile", dependencies: { "@ex/other-lib": "workspace:*" } },
159
+ app("expo"),
160
+ );
161
+ expect(
162
+ await getConsumerAppsTsconfigTarget({
163
+ rootPath,
164
+ packageName: "@ex/lib",
165
+ withBun: false,
166
+ }),
167
+ ).toBe(undefined);
168
+ });
169
+ });
@@ -0,0 +1,8 @@
1
+ // node backend apps: their scripts run in the same environment as their
2
+ // sources, so the package tsconfig.json type-checks them too instead of a
3
+ // separate tsconfig.tools.json
4
+ export const nodeAppTypes = ["node", "node-library", "alp-node"];
5
+
6
+ export const isNodeApp = (pobConfig) =>
7
+ pobConfig?.project?.type === "app" &&
8
+ nodeAppTypes.includes(pobConfig.app?.type);
@@ -58,6 +58,42 @@ export const workspacesRunSequential = (packageManager, script) => {
58
58
  }
59
59
  };
60
60
 
61
+ /**
62
+ * Run a script in the given workspaces only, in topological dependency order.
63
+ * Use to build packages other builds depend on first.
64
+ */
65
+ export const workspacesRunOnly = (packageManager, script, ...packageNames) => {
66
+ switch (packageManager) {
67
+ case undefined:
68
+ case "yarn": {
69
+ const includeArgs = packageNames
70
+ .map((name) => `--include "${name}"`)
71
+ .join(" ");
72
+ return `yarn workspaces foreach --topological-dev ${includeArgs} -Av run ${script}`;
73
+ }
74
+ case "pnpm": {
75
+ const filterArgs = packageNames
76
+ .map((name) => `--filter "${name}"`)
77
+ .join(" ");
78
+ return `pnpm ${filterArgs} run ${script}`;
79
+ }
80
+ case "npm": {
81
+ const workspaceArgs = packageNames
82
+ .map((name) => `--workspace "${name}"`)
83
+ .join(" ");
84
+ return `npm run ${script} ${workspaceArgs}`;
85
+ }
86
+ case "bun": {
87
+ const filterArgs = packageNames
88
+ .map((name) => `--filter '${name}'`)
89
+ .join(" ");
90
+ return `bun ${filterArgs} run ${script}`;
91
+ }
92
+ default:
93
+ throw new Error(`Unsupported package manager: ${packageManager}`);
94
+ }
95
+ };
96
+
61
97
  /**
62
98
  * Run a script in all workspaces in parallel, excluding packages matching the given patterns.
63
99
  * Use for watch commands where example/demo packages should be skipped.
@@ -0,0 +1,17 @@
1
+ const escapeRegExp = (value) => value.replaceAll(/[$()*+.?[\\\]^{|}]/g, "\\$&");
2
+
3
+ /**
4
+ * Names of the workspace packages imported by a rollup config (e.g. a rollup
5
+ * plugin): the root rollup config loads them from their build output, so
6
+ * they must be built before it runs. Only packages with a build script.
7
+ */
8
+ export const getRollupConfigDependencies = (packages, rollupConfigContents) =>
9
+ packages
10
+ .filter((pkg) => {
11
+ if (!pkg.scripts?.build) return false;
12
+ const importRegex = new RegExp(
13
+ `from\\s+["']${escapeRegExp(pkg.name)}(?:/[^"']*)?["']`,
14
+ );
15
+ return rollupConfigContents.some((content) => importRegex.test(content));
16
+ })
17
+ .map((pkg) => pkg.name);