@dbx-tools/projen 0.9.138 → 0.9.139

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/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
5
  export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
6
- export const PACKAGE_VERSION = "0.9.138";
6
+ export const PACKAGE_VERSION = "0.9.139";
7
7
  export * as barrels from "./src/barrels.ts";
8
8
  export * as bunApp from "./src/bun-app.ts";
9
9
  export * as bunWorkflow from "./src/bun-workflow.ts";
package/package.json CHANGED
@@ -30,10 +30,10 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@clack/prompts": "^1.7.0",
33
- "@dbx-tools/cli-args": "^0.9.138",
34
- "@dbx-tools/core": "^0.9.138",
35
- "@dbx-tools/path": "^0.9.138",
36
- "@dbx-tools/shared-core": "^0.9.138",
33
+ "@dbx-tools/cli-args": "^0.9.139",
34
+ "@dbx-tools/core": "^0.9.139",
35
+ "@dbx-tools/path": "^0.9.139",
36
+ "@dbx-tools/shared-core": "^0.9.139",
37
37
  "commander": "^15.0.0",
38
38
  "concurrently": "^10.0.3",
39
39
  "constructs": "^10.6.0",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "main": "index.ts",
57
57
  "license": "Apache-2.0",
58
- "version": "0.9.138",
58
+ "version": "0.9.139",
59
59
  "types": "index.ts",
60
60
  "type": "module",
61
61
  "exports": {
@@ -5,6 +5,7 @@ import { availableParallelism } from "node:os";
5
5
  import { join, resolve } from "node:path";
6
6
  import * as exec from "@dbx-tools/core/exec";
7
7
  import { errorUtils, log } from "@dbx-tools/shared-core";
8
+ import type { TaskSpec, TasksManifest } from "projen";
8
9
  import { runTaskMain } from "./cli.ts";
9
10
 
10
11
  const logger = log.logger("projen:compile");
@@ -15,20 +16,6 @@ interface PackageManifest {
15
16
  readonly workspaces?: string[];
16
17
  }
17
18
 
18
- interface TaskManifest {
19
- readonly tasks?: {
20
- readonly "pre-compile"?: {
21
- readonly steps?: Array<{ readonly exec?: string; readonly execArgs?: string[] }>;
22
- };
23
- readonly compile?: {
24
- readonly steps?: Array<{ readonly exec?: string; readonly execArgs?: string[] }>;
25
- };
26
- readonly "post-compile"?: {
27
- readonly steps?: Array<{ readonly exec?: string; readonly execArgs?: string[] }>;
28
- };
29
- };
30
- }
31
-
32
19
  export interface CompilePlan {
33
20
  readonly typescriptConfigs: string[];
34
21
  readonly customPackages: Array<{ readonly name: string; readonly directory: string }>;
@@ -38,9 +25,15 @@ function readJson<T>(path: string): T {
38
25
  return JSON.parse(readFileSync(path, "utf8")) as T;
39
26
  }
40
27
 
41
- function plainTypeScriptBuild(steps: unknown): boolean {
28
+ function hasTaskOptions(task: TaskSpec | undefined): boolean {
29
+ return Object.keys(task ?? {}).some((key) => !["name", "description", "steps"].includes(key));
30
+ }
31
+
32
+ function plainTypeScriptBuild(task: TaskSpec | undefined): boolean {
33
+ if (hasTaskOptions(task)) return false;
34
+ const steps = task?.steps;
42
35
  if (!Array.isArray(steps) || steps.length !== 1) return false;
43
- const step = steps[0] as { execArgs?: string[] };
36
+ const step = steps[0];
44
37
  if (!step || typeof step !== "object") return false;
45
38
  return (
46
39
  Object.keys(step).length === 1 &&
@@ -50,8 +43,8 @@ function plainTypeScriptBuild(steps: unknown): boolean {
50
43
  );
51
44
  }
52
45
 
53
- function hasSteps(steps: unknown): boolean {
54
- return Array.isArray(steps) && steps.length > 0;
46
+ function hasLifecycle(task: TaskSpec | undefined): boolean {
47
+ return hasTaskOptions(task) || (task?.steps?.length ?? 0) > 0;
55
48
  }
56
49
 
57
50
  /** Read the root's concrete workspace paths on every run, including newly added members. */
@@ -65,13 +58,13 @@ export function compilePlan(root: string): CompilePlan {
65
58
  const script = manifest.scripts?.compile;
66
59
  if (!script) continue;
67
60
  const taskPath = join(directory, ".projen", "tasks.json");
68
- const tasks = existsSync(taskPath) ? readJson<TaskManifest>(taskPath).tasks : undefined;
69
- const plainCompile = plainTypeScriptBuild(tasks?.compile?.steps);
61
+ const tasks = existsSync(taskPath) ? readJson<TasksManifest>(taskPath).tasks : undefined;
62
+ const plainCompile = plainTypeScriptBuild(tasks?.compile);
70
63
  if (
71
64
  script === "projen compile" &&
72
65
  plainCompile &&
73
- !hasSteps(tasks?.["pre-compile"]?.steps) &&
74
- !hasSteps(tasks?.["post-compile"]?.steps)
66
+ !hasLifecycle(tasks?.["pre-compile"]) &&
67
+ !hasLifecycle(tasks?.["post-compile"])
75
68
  ) {
76
69
  typescriptConfigs.push(join(directory, "tsconfig.json"));
77
70
  } else {
@@ -15,13 +15,14 @@ import { tmpdir } from "node:os";
15
15
  import { join, relative, resolve } from "node:path";
16
16
  import * as exec from "@dbx-tools/core/exec";
17
17
  import * as projectUtils from "@dbx-tools/core/project-utils";
18
- import { log } from "@dbx-tools/shared-core";
18
+ import { asyncUtils, log } from "@dbx-tools/shared-core";
19
19
  import { z } from "zod";
20
20
  import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
21
- import { TaskDryRunOptionSchema } from "./options.ts";
22
- import { runTaskCommand } from "../src/_task-command.ts";
21
+ import { TaskConcurrencyOptionSchema, TaskDryRunOptionSchema } from "./options.ts";
22
+ import { runTaskCommandAsync } from "../src/_task-command.ts";
23
23
 
24
24
  const DEFAULT_REGISTRY = "https://registry.npmjs.org";
25
+ const DEFAULT_PUBLISH_CONCURRENCY = 4;
25
26
  const logger = log.logger("projen:publish-npm");
26
27
 
27
28
  /** Registry and archive identity used to verify idempotent npm publication. */
@@ -258,7 +259,8 @@ export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
258
259
  };
259
260
  }
260
261
 
261
- function orderNpmArchives(archives: readonly string[]): string[] {
262
+ /** Group npm archives into dependency-ordered layers that can publish concurrently. */
263
+ function npmArchiveLayers(archives: readonly string[]): string[][] {
262
264
  const byName = new Map(
263
265
  archives.map((archive) => {
264
266
  const manifest = readNpmArchiveManifest(archive);
@@ -278,7 +280,7 @@ function orderNpmArchives(archives: readonly string[]): string[] {
278
280
  ),
279
281
  ]),
280
282
  );
281
- const ordered: string[] = [];
283
+ const layers: string[][] = [];
282
284
  while (remaining.size > 0) {
283
285
  const ready = [...remaining]
284
286
  .filter(([, dependencies]) => dependencies.size === 0)
@@ -289,15 +291,15 @@ function orderNpmArchives(archives: readonly string[]): string[] {
289
291
  `Cyclic npm release dependencies: ${[...remaining.keys()].sort().join(", ")}`,
290
292
  );
291
293
  }
294
+ layers.push(ready.map((name) => byName.get(name)!.archive));
292
295
  for (const name of ready) {
293
- ordered.push(byName.get(name)!.archive);
294
296
  remaining.delete(name);
295
297
  }
296
298
  for (const dependencies of remaining.values()) {
297
299
  for (const name of ready) dependencies.delete(name);
298
300
  }
299
301
  }
300
- return ordered;
302
+ return layers;
301
303
  }
302
304
 
303
305
  /** Hash paths, executable bits, symlink targets, and bytes while ignoring tar metadata. */
@@ -414,41 +416,51 @@ export function packNpmPackage(
414
416
 
415
417
  /** Publish dependency-ordered npm archives, skipping only exact registry matches. */
416
418
  export async function publishNpmArchives(options: {
419
+ readonly concurrency?: number;
417
420
  readonly directory: string;
418
421
  readonly dryRun?: boolean;
419
422
  readonly registry?: string;
420
423
  readonly version?: string;
421
424
  }): Promise<void> {
422
425
  const directory = resolve(options.directory);
423
- const archives = orderNpmArchives(
426
+ const layers = npmArchiveLayers(
424
427
  readdirSync(directory)
425
428
  .filter((file) => file.endsWith(".tgz"))
426
429
  .sort()
427
430
  .map((file) => join(directory, file)),
428
431
  );
429
- if (archives.length === 0) throw new Error(`No npm archives found in ${directory}`);
432
+ if (layers.length === 0) throw new Error(`No npm archives found in ${directory}`);
430
433
 
431
- for (const archive of archives) {
432
- const local = readNpmArchiveIdentity(archive);
433
- if (options.version && local.version !== options.version) {
434
- throw new Error(
435
- `npm archive ${archive} carries ${local.version}, expected ${options.version}`,
436
- );
437
- }
438
- if (!options.dryRun) {
439
- const published = await publishedNpmRelease(local.name, local.version, options.registry);
440
- if (npmReleaseMatches(local, published)) {
441
- logger.info(`skip published ${local.name}@${local.version}`);
442
- continue;
443
- }
444
- }
445
- runTaskCommand(process.cwd(), "npm", [
446
- "publish",
447
- archive,
448
- ...(local.access ? ["--access", local.access] : []),
449
- ...(options.registry ? ["--registry", options.registry] : []),
450
- ...(options.dryRun ? ["--dry-run"] : []),
451
- ]);
434
+ for (const archives of layers) {
435
+ await asyncUtils.mapConcurrent(
436
+ archives,
437
+ async (archive) => {
438
+ const local = readNpmArchiveIdentity(archive);
439
+ if (options.version && local.version !== options.version) {
440
+ throw new Error(
441
+ `npm archive ${archive} carries ${local.version}, expected ${options.version}`,
442
+ );
443
+ }
444
+ if (!options.dryRun) {
445
+ const published = await publishedNpmRelease(local.name, local.version, options.registry);
446
+ if (npmReleaseMatches(local, published)) {
447
+ logger.info(`skip published ${local.name}@${local.version}`);
448
+ return;
449
+ }
450
+ }
451
+ await runTaskCommandAsync(process.cwd(), "npm", [
452
+ "publish",
453
+ archive,
454
+ ...(local.access ? ["--access", local.access] : []),
455
+ ...(options.registry ? ["--registry", options.registry] : []),
456
+ ...(options.dryRun ? ["--dry-run"] : []),
457
+ ]);
458
+ },
459
+ {
460
+ concurrency: options.concurrency ?? DEFAULT_PUBLISH_CONCURRENCY,
461
+ errorMode: "settle",
462
+ },
463
+ );
452
464
  }
453
465
  }
454
466
 
@@ -464,6 +476,7 @@ export const PublishNpmOptionsSchema = z.object({
464
476
  .describe("npm registry URL")
465
477
  .meta({ env: "NPM_CONFIG_REGISTRY" }),
466
478
  dryRun: TaskDryRunOptionSchema,
479
+ concurrency: TaskConcurrencyOptionSchema.default(DEFAULT_PUBLISH_CONCURRENCY),
467
480
  });
468
481
 
469
482
  /** Parse task options and publish validated npm release archives. */
@@ -1,6 +1,7 @@
1
- import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { basename, join, relative, resolve, sep } from "node:path";
3
3
  import * as exec from "@dbx-tools/core/exec";
4
+ import { findFilesSync } from "@dbx-tools/path/find";
4
5
  import { log, object } from "@dbx-tools/shared-core";
5
6
  import { z } from "zod";
6
7
  import { runTaskMain, taskCommand, taskOptions, taskPositionals, taskRoot } from "./cli.ts";
@@ -90,17 +91,14 @@ function writeGraph(root: string, graph: WorkspaceGraph): void {
90
91
  }
91
92
 
92
93
  function testFiles(directory: string): string[] {
93
- if (!existsSync(directory)) return [];
94
- const files: string[] = [];
95
- const visit = (current: string): void => {
96
- for (const entry of readdirSync(current, { withFileTypes: true })) {
97
- const path = join(current, entry.name);
98
- if (entry.isDirectory()) visit(path);
99
- else if (/\.(?:test|spec)\.tsx?$/.test(entry.name)) files.push(path);
100
- }
101
- };
102
- visit(directory);
103
- return files.sort();
94
+ return [
95
+ ...findFilesSync("**/*.{test,spec}.{js,jsx,ts,tsx,mjs,cjs,mts,cts}", {
96
+ cwd: directory,
97
+ absolute: true,
98
+ nodir: true,
99
+ ignoreOptions: { test: false },
100
+ }),
101
+ ].sort();
104
102
  }
105
103
 
106
104
  function integrationTest(path: string): boolean {
@@ -117,13 +115,13 @@ function packageForPath(graph: WorkspaceGraph, path: string): WorkspaceGraphPack
117
115
  .find((pkg) => normalized === pkg.path || normalized.startsWith(`${pkg.path}/`));
118
116
  }
119
117
 
120
- function affectedPackages(graph: WorkspaceGraph, changed: readonly string[]): Set<string> {
118
+ function affectedPackages(graph: WorkspaceGraph, changed: readonly string[]): Set<string> | "all" {
121
119
  const byName = new Map(graph.packages.map((pkg) => [pkg.name, pkg]));
122
120
  const affected = new Set<string>();
123
121
  const pending: string[] = [];
124
122
  for (const path of changed) {
125
123
  const pkg = packageForPath(graph, path);
126
- if (!pkg) return new Set(graph.packages.map((entry) => entry.name));
124
+ if (!pkg) return "all";
127
125
  if (!affected.has(pkg.name)) pending.push(pkg.name);
128
126
  }
129
127
  while (pending.length > 0) {
@@ -175,7 +173,7 @@ function selectedTests(
175
173
  if (mode === "integration") return all.filter(integrationTest);
176
174
  if (mode === "all") return all;
177
175
 
178
- let names: Set<string>;
176
+ let names: Set<string> | "all";
179
177
  if (mode === "changed") {
180
178
  const changed = changedFiles(root, base);
181
179
  if (changed.length === 0) return [];
@@ -190,6 +188,7 @@ function selectedTests(
190
188
  names.add(byPath.name);
191
189
  }
192
190
  }
191
+ if (names === "all") return all;
193
192
  const paths = new Set(graph.packages.filter((pkg) => names.has(pkg.name)).map((pkg) => pkg.path));
194
193
  return all.filter((path) => {
195
194
  const relativePath = toPosix(relative(root, path));