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.
- package/lib/generators/app/PobAppGenerator.js +24 -4
- package/lib/generators/common/format-lint/CommonLintGenerator.js +14 -2
- package/lib/generators/common/husky/CommonHuskyGenerator.js +3 -1
- package/lib/generators/common/testing/CommonTestingGenerator.js +10 -2
- package/lib/generators/common/transpiler/CommonTranspilerGenerator.js +11 -9
- package/lib/generators/common/typescript/CommonTypescriptGenerator.js +110 -38
- package/lib/generators/core/bun/CoreBunGenerator.js +1 -1
- package/lib/generators/core/ci/CoreCIGenerator.js +4 -1
- package/lib/generators/core/git/CoreGitGenerator.js +2 -0
- package/lib/generators/core/git/generators/github/CoreGitGithubGenerator.js +1 -0
- package/lib/generators/core/package/CorePackageGenerator.js +12 -3
- package/lib/generators/core/pnpm/CorePnpmGenerator.js +8 -2
- package/lib/generators/core/vscode/CoreVSCodeGenerator.js +9 -0
- package/lib/generators/core/vscode/templates/extensions.json.ejs +2 -1
- package/lib/generators/core/vscode/templates/settings.json.ejs +5 -0
- package/lib/generators/core/yarn/CoreYarnGenerator.js +6 -2
- package/lib/generators/lib/PobLibGenerator.js +15 -13
- package/lib/generators/monorepo/PobMonorepoGenerator.js +44 -6
- package/lib/generators/monorepo/typescript/MonorepoTypescriptGenerator.js +13 -1
- package/lib/generators/monorepo/typescript/templates/tsconfig.tools.json.ejs +7 -0
- package/lib/generators/monorepo/workspaces/MonorepoWorkspacesGenerator.js +7 -2
- package/lib/generators/pob/PobBaseGenerator.js +4 -2
- package/lib/pob.js +75 -5
- package/lib/utils/alouette.js +18 -0
- package/lib/utils/appTsconfigTarget.js +60 -0
- package/lib/utils/appTsconfigTarget.test.js +169 -0
- package/lib/utils/nodeApps.js +8 -0
- package/lib/utils/packageManagerWorkspacesUtils.js +36 -0
- package/lib/utils/rollupConfigDependencies.js +17 -0
- package/lib/utils/rollupConfigDependencies.test.js +48 -0
- 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":
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|