@dbx-tools/projen 0.6.223 → 0.6.225

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/src/project-js.ts CHANGED
@@ -13,8 +13,10 @@ import { ignore, match } from "@dbx-tools/path";
13
13
  import { object, string, type OneOrMany } from "@dbx-tools/shared-core";
14
14
  import { type IConstruct } from "constructs";
15
15
  import { Component, IgnoreFile, Project, type TaskOptions, javascript, typescript } from "projen";
16
+ import { BuildWorkflow } from "projen/lib/build";
17
+ import { AutoMerge } from "projen/lib/github";
16
18
  import { JobPermission, type JobStep } from "projen/lib/github/workflows-model";
17
- import { ReleaseTrigger } from "projen/lib/release";
19
+ import type { ReleaseProjectOptions } from "projen/lib/release";
18
20
  import { mixin } from "..";
19
21
  import { generateBarrels } from "./barrels.ts";
20
22
  import {
@@ -33,14 +35,13 @@ import {
33
35
  type DiscoveredPackage,
34
36
  projectName,
35
37
  readPackageManifest,
36
- repoRoot,
37
38
  scanPackages,
38
39
  toPosix,
39
40
  } from "./packages.ts";
40
41
  import { PnpmWorkspaceState, type DBXToolsPNPMWorkspaceOptions } from "./pnpm-workspace.ts";
41
42
  import type { DBXToolsProject, DBXToolsProjectOptions as CommonProjectOptions } from "./project.ts";
42
43
  import { applyCompiledPublish } from "./publish.ts";
43
- import { DBXToolsRelease, type ReleaseDocsOptions } from "./release.ts";
44
+ import { DBXToolsRelease, type ReleaseDocsOptions, type ReleaseSummaryOptions } from "./release.ts";
44
45
  import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags.ts";
45
46
  import { DBXToolsRootTsconfig } from "./tsconfig.ts";
46
47
  import { DBXToolsVsCode } from "./vscode.ts";
@@ -67,6 +68,8 @@ export interface DBXToolsJavaScriptProject extends DBXToolsProject, javascript.N
67
68
  rootTsconfig?: DBXToolsRootTsconfig;
68
69
  /** Root `.vscode/*` - only a tree ROOT has one. */
69
70
  vsCode?: DBXToolsVsCode;
71
+ /** Repository outputs excluded from generated workflow dependency-cache hashing. */
72
+ readonly workflowCacheIgnorePaths: readonly string[];
70
73
  }
71
74
 
72
75
  /** Parsed npm package identifier: optional scope plus the unscoped package name. */
@@ -146,10 +149,11 @@ function configureRootPackage(project: javascript.NodeProject): void {
146
149
  * a normalized git remote), in npm's `git+https://.../repo.git` form.
147
150
  */
148
151
  function applyRepository(project: javascript.NodeProject, override?: string): void {
149
- const url = override && override.length ? override : coreProject.repositoryUrl(repoRoot, "npm");
152
+ const workspaceRoot = resolve(project.root.outdir);
153
+ const url =
154
+ override && override.length ? override : coreProject.repositoryUrl(workspaceRoot, "npm");
150
155
  if (!url) return;
151
- const root = project.parent ?? project;
152
- const directory = toPosix(relative(resolve(root.outdir), resolve(project.outdir)));
156
+ const directory = toPosix(relative(workspaceRoot, resolve(project.outdir)));
153
157
  project.package.addField("repository", {
154
158
  type: "git",
155
159
  url,
@@ -389,9 +393,7 @@ export const DBX_TOOLS_LICENSE = "Apache-2.0";
389
393
  * inherits the root's config rather than emitting its own. `name`/`defaultReleaseBranch`
390
394
  * are resolved/applied by the caller.
391
395
  */
392
- function defaultProjectOptions(
393
- options: DBXToolsJavaScriptProjectOptions,
394
- ): DBXToolsJavaScriptProjectOptions {
396
+ function defaultProjectOptions(options: DBXToolsJavaScriptProjectOptions) {
395
397
  const isRoot = options.parent === undefined;
396
398
  return {
397
399
  // Bun owns install/run/build/test locally and in CI. projen renders
@@ -423,9 +425,7 @@ function defaultProjectOptions(
423
425
  // to Verdaccio still work without a CI OIDC provider. See
424
426
  // {@link DBXToolsRelease}.
425
427
  ...(isRoot ? {} : { npmAccess: javascript.NpmAccess.PUBLIC }),
426
- buildWorkflow: false,
427
428
  workflowPackageCache: false,
428
- release: false,
429
429
  // The root build validates the whole workspace and must not also pack every
430
430
  // member into unused `dist/js` tarballs. Child projects keep projen's package
431
431
  // task so `bun run build` in ONE package remains a complete compile/test/pack
@@ -460,6 +460,8 @@ function defaultProjectOptions(
460
460
  }
461
461
  : {}),
462
462
  ...options,
463
+ buildWorkflow: false,
464
+ release: false,
463
465
  githubOptions: {
464
466
  ...options.githubOptions,
465
467
  pullRequestLint: false,
@@ -493,9 +495,7 @@ function copiedGitIgnoreOptions(
493
495
  * same parent-based root/child logic applies; this just layers on typescript +
494
496
  * bun types and disables sample code. No `tsx`: bun runs `.ts` directly.
495
497
  */
496
- function defaultTypeScriptProjectOptions(
497
- options: DBXToolsTypeScriptProjectOptions,
498
- ): DBXToolsTypeScriptProjectOptions {
498
+ function defaultTypeScriptProjectOptions(options: DBXToolsTypeScriptProjectOptions) {
499
499
  const base = defaultProjectOptions(options);
500
500
  return {
501
501
  ...base,
@@ -513,84 +513,154 @@ function defaultTypeScriptProjectOptions(
513
513
  // Pinned to match the subproject defaults so bun resolves one TypeScript and
514
514
  // one runtime-compatible set of Bun globals across the workspace.
515
515
  const DEV_DEPS_ROOT: string[] = ["typescript@^5.9.3", `@types/bun@${BUN_VERSION}`];
516
+ const NATIVE_RELEASE_OPTIONS = [
517
+ "jsiiReleaseVersion",
518
+ "majorVersion",
519
+ "minMajorVersion",
520
+ "npmDistTag",
521
+ "prerelease",
522
+ "publishDryRun",
523
+ "publishTasks",
524
+ "releasableCommits",
525
+ "release",
526
+ "releaseBranches",
527
+ "releaseEnvironment",
528
+ "releaseEveryCommit",
529
+ "releaseFailureIssue",
530
+ "releaseFailureIssueLabel",
531
+ "releaseToNpm",
532
+ "releaseTrigger",
533
+ "releaseWorkflowName",
534
+ "releaseWorkflowSetupSteps",
535
+ "versionrcOptions",
536
+ ] as const;
537
+
538
+ function validateReleaseOptions(options: DBXToolsJavaScriptProjectOptions): void {
539
+ for (const key of NATIVE_RELEASE_OPTIONS) {
540
+ if ((options as Record<string, unknown>)[key] !== undefined) {
541
+ throw new Error(
542
+ `${key} is a native Projen release option; use dbx-tools release options instead`,
543
+ );
544
+ }
545
+ }
546
+ if (
547
+ options.releaseMode === "disabled" &&
548
+ (options.releaseDocs !== undefined || options.nodeRelease !== undefined)
549
+ ) {
550
+ throw new Error("releaseMode disabled cannot be combined with releaseDocs or nodeRelease");
551
+ }
552
+ }
516
553
 
517
554
  /** Options for {@link DBXToolsNodeProject} (the monorepo root). */
518
- export interface DBXToolsJavaScriptProjectOptions
519
- extends
520
- CommonProjectOptions,
521
- Partial<javascript.NodeProjectOptions>,
522
- DBXToolsConfigOptions,
523
- DBXToolsPNPMWorkspaceOptions {
524
- /**
525
- * The npm scope for generated package names (`@<scope>/<seg-...>`). Defaults to
526
- * the (resolved) project name; a leading `@` is optional.
527
- */
528
- readonly scope?: string;
529
- /**
530
- * Roots scanned for packages (each `src`-bearing folder under a root is one).
531
- * Only a ROOT scans. Defaults to {@link DEFAULT_PACKAGE_ROOTS}.
532
- */
533
- readonly packageRoots?: readonly string[];
534
- /**
535
- * Descriptions for published packages, keyed by repository-relative package
536
- * directory. When configured, synthesis rejects every public JavaScript
537
- * package whose final manifest has no non-empty description.
538
- */
539
- readonly packageDescriptions?: Readonly<Record<string, string>>;
540
- /**
541
- * Leading path segment(s) dropped from a discovered package's relative path
542
- * before its npm name is derived, so a tier folder doesn't become a name
543
- * prefix. E.g. with the default `"node"`, `packages/node/path` names as
544
- * `@<scope>/path` instead of `@<scope>/node-path` (its `node` TAG still
545
- * derives from the path). One or many segment names; a segment is only
546
- * stripped when it is the FIRST segment of the relative path. Pass `[]` to
547
- * disable. Defaults to `"node"`.
548
- */
549
- readonly omitRelativePrefix?: OneOrMany<string>;
550
- /**
551
- * Maps a path token / relPath / glob to tag(s), unioned into a package's
552
- * path-derived tags. Defaults to an identity map over the known tag names; a
553
- * `""`/`"."` key tags the root.
554
- */
555
- readonly packageTagPaths?: Record<string, string[]>;
556
- /**
557
- * Which built-in {@link PACKAGE_TAG_MIXINS} to apply and seed
558
- * `packageTagPaths` identity entries for. Omitted = all; `false` = none;
559
- * a list = only those tags.
560
- */
561
- readonly defaultTagMixins?: false | PackageTag[];
562
- /**
563
- * Extra repo-root paths that trigger a full re-synth during `sync --watch`
564
- * (alongside `.projenrc.ts`). Repo-relative, e.g. `".example.projenrc.ts"`.
565
- */
566
- readonly syncResynthPaths?: readonly string[];
567
- /** GitHub Pages documentation included in the unified release workflow. */
568
- readonly releaseDocs?: ReleaseDocsOptions;
569
- /** Set to `false` to omit normal npm workspace publication. */
570
- readonly nodeRelease?: boolean;
571
- /**
572
- * Extra workspace member paths (repo-relative, POSIX) to list in the workspace
573
- * config ALONGSIDE the discovered `packageRoots` members - for a package that
574
- * is synthesized by its OWN `.projenrc.ts` (so it isn't a root subproject) but
575
- * should still resolve as a workspace sibling. The `@dbx-tools/projen` engine in
576
- * `projen/` is the case: it synthesizes itself (avoiding a dogfooding cycle) yet
577
- * is a member of the single bun workspace, so the root links it from source.
578
- */
579
- readonly extraWorkspaceMembers?: readonly string[];
580
- /**
581
- * Install workspace dependencies once from the custom root instead of once
582
- * per child project during post-synthesis. Defaults to `true`; set `false` to
583
- * preserve projen's native per-project install tasks.
584
- */
585
- readonly rootInstallOnly?: boolean;
555
+ export type DBXToolsReleaseMode = "dbx-tools" | "disabled";
556
+
557
+ /** Optional semantic pull request title policy for the generated build workflow. */
558
+ export interface PullRequestTitlePolicyOptions {
559
+ /** Allowed semantic title types. */
560
+ readonly types: readonly string[];
561
+ /** Require a semantic title scope. Defaults to `false`. */
562
+ readonly requireScope?: boolean;
586
563
  }
587
564
 
565
+ type SupportedWorkflowOptions = Pick<
566
+ ReleaseProjectOptions,
567
+ "postBuildSteps" | "workflowContainerImage" | "workflowRunsOn" | "workflowRunsOnGroup"
568
+ >;
569
+
570
+ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
571
+ Partial<
572
+ Omit<javascript.NodeProjectOptions, keyof ReleaseProjectOptions | "release" | "releaseToNpm">
573
+ > &
574
+ Partial<SupportedWorkflowOptions> &
575
+ DBXToolsConfigOptions &
576
+ DBXToolsPNPMWorkspaceOptions & {
577
+ /**
578
+ * The npm scope for generated package names (`@<scope>/<seg-...>`). Defaults to
579
+ * the (resolved) project name; a leading `@` is optional.
580
+ */
581
+ readonly scope?: string;
582
+ /**
583
+ * Roots scanned for packages (each `src`-bearing folder under a root is one).
584
+ * Only a ROOT scans. Defaults to {@link DEFAULT_PACKAGE_ROOTS}.
585
+ */
586
+ readonly packageRoots?: readonly string[];
587
+ /**
588
+ * Descriptions for published packages, keyed by repository-relative package
589
+ * directory. When configured, synthesis rejects every public JavaScript
590
+ * package whose final manifest has no non-empty description.
591
+ */
592
+ readonly packageDescriptions?: Readonly<Record<string, string>>;
593
+ /**
594
+ * Leading path segment(s) dropped from a discovered package's relative path
595
+ * before its npm name is derived, so a tier folder doesn't become a name
596
+ * prefix. E.g. with the default `"node"`, `packages/node/path` names as
597
+ * `@<scope>/path` instead of `@<scope>/node-path` (its `node` TAG still
598
+ * derives from the path). One or many segment names; a segment is only
599
+ * stripped when it is the FIRST segment of the relative path. Pass `[]` to
600
+ * disable. Defaults to `"node"`.
601
+ */
602
+ readonly omitRelativePrefix?: OneOrMany<string>;
603
+ /**
604
+ * Maps a path token / relPath / glob to tag(s), unioned into a package's
605
+ * path-derived tags. Defaults to an identity map over the known tag names; a
606
+ * `""`/`"."` key tags the root.
607
+ */
608
+ readonly packageTagPaths?: Record<string, string[]>;
609
+ /**
610
+ * Which built-in {@link PACKAGE_TAG_MIXINS} to apply and seed
611
+ * `packageTagPaths` identity entries for. Omitted = all; `false` = none;
612
+ * a list = only those tags.
613
+ */
614
+ readonly defaultTagMixins?: false | PackageTag[];
615
+ /**
616
+ * Extra repo-root paths that trigger a full re-synth during `sync --watch`
617
+ * (alongside `.projenrc.ts`). Repo-relative, e.g. `".example.projenrc.ts"`.
618
+ */
619
+ readonly syncResynthPaths?: readonly string[];
620
+ /** GitHub Pages documentation included in the unified release workflow. */
621
+ readonly releaseDocs?: ReleaseDocsOptions;
622
+ /** Python package root passed to local release preparation when configured. */
623
+ readonly releasePythonRoot?: string;
624
+ /** Optional AI-generated release summary. Defaults to enabled. */
625
+ readonly releaseSummary?: boolean | ReleaseSummaryOptions;
626
+ /** Set to `false` to omit normal npm workspace publication. */
627
+ readonly nodeRelease?: boolean;
628
+ /** Unified dbx-tools release workflow, or no release surface. Defaults to `dbx-tools`. */
629
+ readonly releaseMode?: DBXToolsReleaseMode;
630
+ /** Prefix for generated release tags. Defaults to `v`. */
631
+ readonly releaseTagPrefix?: string;
632
+ /** Semantic PR title validation policy. Omitted or `false` disables title validation. */
633
+ readonly pullRequestTitlePolicy?: false | PullRequestTitlePolicyOptions;
634
+ /** Repository output paths excluded from generated workflow dependency-cache hashing. */
635
+ readonly workflowCacheIgnorePaths?: readonly string[];
636
+ /**
637
+ * Extra workspace member paths (repo-relative, POSIX) to list in the workspace
638
+ * config ALONGSIDE the discovered `packageRoots` members - for a package that
639
+ * is synthesized by its OWN `.projenrc.ts` (so it isn't a root subproject) but
640
+ * should still resolve as a workspace sibling. The `@dbx-tools/projen` engine in
641
+ * `projen/` is the case: it synthesizes itself (avoiding a dogfooding cycle) yet
642
+ * is a member of the single bun workspace, so the root links it from source.
643
+ */
644
+ readonly extraWorkspaceMembers?: readonly string[];
645
+ /**
646
+ * Install workspace dependencies once from the custom root instead of once
647
+ * per child project during post-synthesis. Defaults to `true`; set `false` to
648
+ * preserve projen's native per-project install tasks.
649
+ */
650
+ readonly rootInstallOnly?: boolean;
651
+ };
652
+
588
653
  /** Options for {@link DBXToolsTypeScriptProject} (a package, or a compiling root). */
589
- export interface DBXToolsTypeScriptProjectOptions
590
- extends Partial<typescript.TypeScriptProjectOptions>, DBXToolsJavaScriptProjectOptions {
591
- /** Emit the projen-owned bun app scaffolding (`bunfig.toml`/`dev.ts`/`build.ts`). */
592
- readonly bunApp?: boolean;
593
- }
654
+ export type DBXToolsTypeScriptProjectOptions = Partial<
655
+ Omit<
656
+ typescript.TypeScriptProjectOptions,
657
+ keyof ReleaseProjectOptions | "release" | "releaseToNpm"
658
+ >
659
+ > &
660
+ DBXToolsJavaScriptProjectOptions & {
661
+ /** Emit the projen-owned bun app scaffolding (`bunfig.toml`/`dev.ts`/`build.ts`). */
662
+ readonly bunApp?: boolean;
663
+ };
594
664
 
595
665
  /**
596
666
  * A monorepo root. Scans `packageRoots` and appends a
@@ -609,14 +679,12 @@ export class DBXToolsNodeProject
609
679
  vsCode?: DBXToolsVsCode;
610
680
  readonly extraWorkspaceMembers: readonly string[];
611
681
  readonly releaseBranch: string;
682
+ readonly workflowCacheIgnorePaths: readonly string[];
612
683
  private readonly rootInstallOnly: boolean;
613
684
 
614
685
  constructor(options: DBXToolsJavaScriptProjectOptions = {}) {
686
+ validateReleaseOptions(options);
615
687
  const { name, scope } = resolveIdentity(options);
616
- const releaseDefaults =
617
- options.release && options.releaseTrigger === undefined
618
- ? { releaseTrigger: ReleaseTrigger.tagged({ tags: ["v*"] }) }
619
- : {};
620
688
  // Holds the workspace state (members/catalog/allowBuilds/overrides). Under
621
689
  // bun, projen's base constructor does NOT create the `PnpmWorkspaceYaml`
622
690
  // component (its `configurePnpm` call site is gated to pnpm), so this state's
@@ -626,7 +694,6 @@ export class DBXToolsNodeProject
626
694
  const pnpmWorkspace = new PnpmWorkspaceState(options);
627
695
  super({
628
696
  ...defaultProjectOptions(options),
629
- ...releaseDefaults,
630
697
  pnpmOptions: {
631
698
  ...options.pnpmOptions,
632
699
  workspaceYamlOptions: pnpmWorkspace.options,
@@ -645,8 +712,10 @@ export class DBXToolsNodeProject
645
712
  this.scope = scope;
646
713
  this.extraWorkspaceMembers = options.extraWorkspaceMembers ?? [];
647
714
  this.releaseBranch = options.defaultReleaseBranch ?? "main";
715
+ this.workflowCacheIgnorePaths = options.workflowCacheIgnorePaths ?? [];
648
716
  this.rootInstallOnly = options.rootInstallOnly !== false;
649
717
  this.dbxToolsConfig = new DBXToolsConfig(this, options);
718
+ configureBuildWorkflow(this, options);
650
719
  initProject(this, options);
651
720
  }
652
721
 
@@ -654,7 +723,9 @@ export class DBXToolsNodeProject
654
723
  const steps = super.renderWorkflowSetup(options);
655
724
  if (this.parent) return steps;
656
725
  return steps.flatMap((step) => {
657
- if (step.uses === "oven-sh/setup-bun@v2") return [...bunCacheRestoreSteps(this)];
726
+ if (step.uses === "oven-sh/setup-bun@v2") {
727
+ return [...bunCacheRestoreSteps(this, { ignorePaths: this.workflowCacheIgnorePaths })];
728
+ }
658
729
  if (step.run === "bun install") {
659
730
  return [
660
731
  step,
@@ -669,16 +740,9 @@ export class DBXToolsNodeProject
669
740
  }
670
741
 
671
742
  public override preSynthesize(): void {
672
- if (this.rootInstallOnly) this.with(ROOT_INSTALL_ONLY_MIXIN);
743
+ prepareRootSynthesis(this, this.rootInstallOnly);
673
744
  super.preSynthesize();
674
- const version = readWorkspaceVersion(this.outdir);
675
- for (const member of this.extraWorkspaceMembers) {
676
- syncWorkspaceManifestVersion(join(this.outdir, member, "package.json"), version);
677
- }
678
- // Members come from the attached subprojects, which the root's scan appends
679
- // after construction - so the list is filled here, not in the constructor.
680
- // `extraWorkspaceMembers` adds self-synthesizing siblings (e.g. `projen/`).
681
- this.pnpmWorkspace?.resolveMembers(this, this.extraWorkspaceMembers);
745
+ resolveRootWorkspace(this, this.extraWorkspaceMembers);
682
746
  preSynthesizeProject(this);
683
747
  }
684
748
  }
@@ -726,10 +790,16 @@ export class DBXToolsTypeScriptProject
726
790
  pnpmWorkspace?: PnpmWorkspaceState;
727
791
  rootTsconfig?: DBXToolsRootTsconfig;
728
792
  vsCode?: DBXToolsVsCode;
793
+ readonly extraWorkspaceMembers: readonly string[];
794
+ readonly releaseBranch: string;
795
+ readonly workflowCacheIgnorePaths: readonly string[];
796
+ private readonly rootInstallOnly: boolean;
729
797
 
730
798
  constructor(options: DBXToolsTypeScriptProjectOptions) {
799
+ validateReleaseOptions(options);
731
800
  const { name, scope } = resolveIdentity(options);
732
801
  const parent = options?.parent;
802
+ const pnpmWorkspace = parent ? undefined : new PnpmWorkspaceState(options);
733
803
  const packageManager =
734
804
  options.packageManager ??
735
805
  inheritedPackageManager(parent instanceof javascript.NodeProject ? parent : undefined);
@@ -738,6 +808,14 @@ export class DBXToolsTypeScriptProject
738
808
  ...defaultTypeScriptProjectOptions(options),
739
809
  name: options.name ?? name,
740
810
  packageManager,
811
+ ...(pnpmWorkspace
812
+ ? {
813
+ pnpmOptions: {
814
+ ...options.pnpmOptions,
815
+ workspaceYamlOptions: pnpmWorkspace.options,
816
+ },
817
+ }
818
+ : {}),
741
819
  tsconfig: {
742
820
  ...options.tsconfig,
743
821
  include: options.tsconfig?.include,
@@ -750,7 +828,13 @@ export class DBXToolsTypeScriptProject
750
828
  },
751
829
  },
752
830
  });
831
+ this.pnpmWorkspace = pnpmWorkspace;
832
+ pnpmWorkspace?.attachWorkspaceFile(this);
753
833
  this.scope = scope;
834
+ this.extraWorkspaceMembers = options.extraWorkspaceMembers ?? [];
835
+ this.releaseBranch = options.defaultReleaseBranch ?? "main";
836
+ this.workflowCacheIgnorePaths = options.workflowCacheIgnorePaths ?? [];
837
+ this.rootInstallOnly = options.rootInstallOnly !== false;
754
838
  // Pairs with `jsx` in SHARED_COMPILER_OPTIONS: projen's default `include` is
755
839
  // `src/**/*.ts` only, which silently omits a `.tsx` file from the program
756
840
  // instead of failing, so authoring a React component would otherwise need
@@ -772,27 +856,106 @@ export class DBXToolsTypeScriptProject
772
856
  this.package.addField("version", readWorkspaceVersion(this.root.outdir));
773
857
  addPackageFiles(this, "index.ts", "src");
774
858
  // `bun test` intercepts `node:test` (the suites keep using node:test) and
775
- // runs it with bun's own fast runner. Args are FILTERS, not globs; a bare
776
- // directory auto-discovers `*.test.ts` recursively. But `bun test` EXITS 1
777
- // when it matches no files, so guard it: only invoke when a `*.test.ts`
778
- // exists, else succeed.
779
- this.testTask.exec("bun test test", {
780
- condition: 'find test -name "*.test.ts" 2>/dev/null | grep -q .',
781
- });
859
+ // runs it with bun's own fast runner. The native no-tests option is portable
860
+ // across Windows and Unix and lets packages without tests remain a no-op.
861
+ this.testTask.exec("bun test test --pass-with-no-tests");
782
862
  if (options.bunApp ?? false) {
783
863
  new BunfigFile(this);
784
864
  new BunDevServerFile(this);
785
865
  new BunBuildFile(this);
786
866
  }
867
+ configureBuildWorkflow(this, options);
787
868
  initProject(this, options);
788
869
  }
789
870
 
790
871
  public override preSynthesize(): void {
872
+ prepareRootSynthesis(this, this.rootInstallOnly);
791
873
  super.preSynthesize();
874
+ resolveRootWorkspace(this, this.extraWorkspaceMembers);
792
875
  preSynthesizeProject(this);
793
876
  }
794
877
  }
795
878
 
879
+ function configureBuildWorkflow(
880
+ project: DBXToolsNodeProject | DBXToolsTypeScriptProject,
881
+ options: DBXToolsJavaScriptProjectOptions,
882
+ ): void {
883
+ if (project.parent || !options.buildWorkflow || !project.github) return;
884
+ const validation = project.addTask("pr:validate", {
885
+ description: "Validate generated files and TypeScript packages",
886
+ });
887
+ validation.exec("bunx projen default");
888
+ validation.exec("bun run compile");
889
+ const configured = options.buildWorkflowOptions ?? {};
890
+ const compatibility = nodeWorkflowCompatibility(project);
891
+ const configuredRunner = configured.runsOn !== undefined || configured.runsOnGroup !== undefined;
892
+ const workflow = new BuildWorkflow(project, {
893
+ buildTask: validation,
894
+ artifactsDirectory: project.artifactsDirectory,
895
+ containerImage: options.workflowContainerImage,
896
+ gitIdentity: options.workflowGitIdentity,
897
+ permissions: { idToken: compatibility.determineIdTokenPermissions(options) },
898
+ ...configured,
899
+ preBuildSteps: [
900
+ ...project.renderWorkflowSetup({
901
+ installStepConfiguration: {
902
+ workingDirectory: compatibility.determineInstallWorkingDirectory(),
903
+ },
904
+ mutable: configured.mutableInstall ?? configured.mutableBuild ?? true,
905
+ }),
906
+ ...(configured.preBuildSteps ?? []),
907
+ ],
908
+ postBuildSteps: [...(options.postBuildSteps ?? [])],
909
+ runsOn: configuredRunner ? configured.runsOn : options.workflowRunsOn,
910
+ runsOnGroup: configuredRunner ? configured.runsOnGroup : options.workflowRunsOnGroup,
911
+ });
912
+ workflow.addPostBuildSteps(...compatibility.renderUploadCoverageJobStep(options));
913
+ compatibility.buildWorkflow = workflow;
914
+
915
+ if ((options.autoMerge ?? true) && project.github.mergify) {
916
+ const autoMerge = new AutoMerge(project.github, options.autoMergeOptions);
917
+ autoMerge.addConditionsLater({
918
+ render: () => workflow.buildJobIds.map((id) => `status-success=${id}`),
919
+ });
920
+ compatibility.autoMerge = autoMerge;
921
+ }
922
+ }
923
+
924
+ interface NodeWorkflowCompatibility {
925
+ autoMerge?: AutoMerge;
926
+ buildWorkflow?: BuildWorkflow;
927
+ determineIdTokenPermissions(options: DBXToolsJavaScriptProjectOptions): JobPermission | undefined;
928
+ determineInstallWorkingDirectory(): string | undefined;
929
+ renderUploadCoverageJobStep(options: DBXToolsJavaScriptProjectOptions): JobStep[];
930
+ }
931
+
932
+ /**
933
+ * Access the narrow NodeProject workflow hooks that have no public equivalent.
934
+ * BuildWorkflow itself is created exclusively through its public constructor.
935
+ */
936
+ function nodeWorkflowCompatibility(project: javascript.NodeProject): NodeWorkflowCompatibility {
937
+ return project as unknown as NodeWorkflowCompatibility;
938
+ }
939
+
940
+ function prepareRootSynthesis(
941
+ project: DBXToolsNodeProject | DBXToolsTypeScriptProject,
942
+ rootInstallOnly: boolean,
943
+ ): void {
944
+ if (!project.parent && rootInstallOnly) project.with(ROOT_INSTALL_ONLY_MIXIN);
945
+ }
946
+
947
+ function resolveRootWorkspace(
948
+ project: DBXToolsNodeProject | DBXToolsTypeScriptProject,
949
+ extraWorkspaceMembers: readonly string[],
950
+ ): void {
951
+ if (project.parent) return;
952
+ const version = readWorkspaceVersion(project.outdir);
953
+ for (const member of extraWorkspaceMembers) {
954
+ syncWorkspaceManifestVersion(join(project.outdir, member, "package.json"), version);
955
+ }
956
+ project.pnpmWorkspace?.resolveMembers(project, extraWorkspaceMembers);
957
+ }
958
+
796
959
  /**
797
960
  * Regenerates the repo's generated source after synth: first the codegen
798
961
  * modules (ts-to-zod schemas from each `codegen`-declaring package's upstream
@@ -808,8 +971,10 @@ export class DBXToolsTypeScriptProject
808
971
  */
809
972
  class GeneratedSource extends Component {
810
973
  public override postSynthesize(): void {
811
- generateCodegen();
812
- generateBarrels();
974
+ const projectRoot = this.project.outdir;
975
+ const includeRoot = this.project instanceof DBXToolsTypeScriptProject;
976
+ generateCodegen(projectRoot, { includeRoot });
977
+ generateBarrels({ projectRoot, includeRoot });
813
978
  }
814
979
  }
815
980
 
@@ -837,7 +1002,10 @@ class WorkspaceValidationTasks extends Component {
837
1002
  public override preSynthesize(): void {
838
1003
  if (this.configured) return;
839
1004
  this.configured = true;
840
- const project = this.project as javascript.NodeProject;
1005
+ const project = this.project as DBXToolsNodeProject | DBXToolsTypeScriptProject;
1006
+ if (project.subprojects.length === 0 && project.extraWorkspaceMembers.length === 0) {
1007
+ return;
1008
+ }
841
1009
  for (const task of [project.compileTask, project.testTask]) {
842
1010
  task.exec(`bun run --filter '*' ${task.name}`);
843
1011
  }
@@ -852,29 +1020,17 @@ class WorkspaceValidationTasks extends Component {
852
1020
  * off) do not gain new files.
853
1021
  */
854
1022
  class WorkflowDefaults extends Component {
1023
+ constructor(
1024
+ project: javascript.NodeProject,
1025
+ private readonly pullRequestTitlePolicy: false | PullRequestTitlePolicyOptions | undefined,
1026
+ ) {
1027
+ super(project);
1028
+ }
1029
+
855
1030
  public override preSynthesize(): void {
856
1031
  const project = this.project as javascript.NodeProject;
857
1032
  const workflow = project.buildWorkflow?.workflow;
858
- if (workflow) {
859
- const job = workflow.getJob("build");
860
- if ("steps" in job) {
861
- const mutableJob = job as unknown as { steps: JobStep[] | (() => JobStep[]) };
862
- const configuredSteps = mutableJob.steps;
863
- const rewrite = (steps: JobStep[]): JobStep[] =>
864
- steps.map((step) =>
865
- step.name === "build"
866
- ? {
867
- ...step,
868
- name: "Validate generated files and types",
869
- run: "bunx projen default\nbun run compile",
870
- }
871
- : step,
872
- );
873
- mutableJob.steps =
874
- typeof configuredSteps === "function"
875
- ? () => rewrite(configuredSteps())
876
- : rewrite(configuredSteps);
877
- }
1033
+ if (workflow && this.pullRequestTitlePolicy) {
878
1034
  workflow.addJob("pr-title", {
879
1035
  name: "Validate PR title",
880
1036
  runsOn: ["ubuntu-latest"],
@@ -886,8 +1042,8 @@ class WorkflowDefaults extends Component {
886
1042
  uses: "amannn/action-semantic-pull-request@v6",
887
1043
  env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" },
888
1044
  with: {
889
- types: ["feat", "fix", "chore"].join("\n"),
890
- requireScope: false,
1045
+ types: this.pullRequestTitlePolicy.types.join("\n"),
1046
+ requireScope: this.pullRequestTitlePolicy.requireScope ?? false,
891
1047
  },
892
1048
  },
893
1049
  ],
@@ -995,11 +1151,19 @@ function packageNameFor(scope: string, relPath: string, omitPrefixes: string[]):
995
1151
  * either way it is parsed through {@link PackageIdentifier} so a scoped value
996
1152
  * (`@dbx-tools` or a full `@dbx-tools/root` name) yields the bare scope `dbx-tools`.
997
1153
  */
998
- function resolveIdentity(options: { name?: string; scope?: string }): {
1154
+ function resolveIdentity(options: {
1155
+ name?: string;
1156
+ scope?: string;
1157
+ outdir?: string;
1158
+ parent?: Project;
1159
+ }): {
999
1160
  name: string;
1000
1161
  scope: string;
1001
1162
  } {
1002
- const name = options.name && options.name.length ? options.name : projectName();
1163
+ const projectRoot = options.parent
1164
+ ? resolve(options.parent.outdir, options.outdir ?? ".")
1165
+ : resolve(options.outdir ?? process.cwd());
1166
+ const name = options.name && options.name.length ? options.name : projectName(projectRoot);
1003
1167
  const rawScope = options.scope && options.scope.length ? options.scope : name;
1004
1168
  const identifier = PackageIdentifier.parse(rawScope);
1005
1169
  return { name, scope: identifier?.scope ?? identifier?.name ?? rawScope };
@@ -1227,9 +1391,7 @@ function initProject(
1227
1391
  project.annotateGenerated(`/${root}/**/index.ts`);
1228
1392
  project.annotateGenerated(`/${root}/openapi/**`);
1229
1393
  }
1230
- const extraWorkspaceMembers =
1231
- project instanceof DBXToolsNodeProject ? project.extraWorkspaceMembers : [];
1232
- for (const member of extraWorkspaceMembers) {
1394
+ for (const member of project.extraWorkspaceMembers) {
1233
1395
  project.annotateGenerated(`/${member}/index.ts`);
1234
1396
  }
1235
1397
 
@@ -1241,13 +1403,17 @@ function initProject(
1241
1403
  // repairs the worktree it is meant to validate; `eslint:fix` is the explicit
1242
1404
  // local mutation path.
1243
1405
  const eslint = new javascript.Eslint(project, {
1244
- dirs: [...roots, "projen"],
1406
+ dirs: [...roots, ...project.extraWorkspaceMembers],
1245
1407
  fileExtensions: [".ts", ".tsx"],
1246
1408
  projectService: true,
1247
1409
  prettier: Boolean(project.prettier),
1248
- tsconfigPath: "./tsconfig.json",
1410
+ tsconfigPath:
1411
+ project instanceof typescript.TypeScriptProject
1412
+ ? (project.tsconfig?.fileName ?? "./tsconfig.json")
1413
+ : "./tsconfig.json",
1249
1414
  commandOptions: { fix: false },
1250
1415
  });
1416
+ eslint.allowDefaultProjectFiles(".projenrc.ts");
1251
1417
  project.addTask("eslint:fix", {
1252
1418
  description: "Fix ESLint issues across the codebase",
1253
1419
  exec: "bun run eslint -- --fix",
@@ -1260,7 +1426,9 @@ function initProject(
1260
1426
  eslint.addIgnorePattern(`${root}/**/index.ts`);
1261
1427
  }
1262
1428
  eslint.addIgnorePattern("**/src/generated/**");
1263
- eslint.addIgnorePattern("projen/index.ts");
1429
+ for (const member of project.extraWorkspaceMembers) {
1430
+ eslint.addIgnorePattern(`${member}/index.ts`);
1431
+ }
1264
1432
  // The generated bun app scripts + unmanaged overrides live at the package root,
1265
1433
  // outside any `src/**` tsconfig include, so the type-aware parser cannot resolve
1266
1434
  // them to a project. ESLint still cannot parse them.
@@ -1362,17 +1530,21 @@ function initProject(
1362
1530
  }
1363
1531
 
1364
1532
  new WorkspaceValidationTasks(project);
1365
- new WorkflowDefaults(project);
1533
+ new WorkflowDefaults(project, options.pullRequestTitlePolicy);
1366
1534
  new PrettierIgnoreGenerated(project);
1367
1535
 
1368
1536
  new GeneratedSource(project);
1369
1537
  // Version mutation, reviewed release preparation, and public publication are
1370
1538
  // separate surfaces owned by one release component.
1371
- new DBXToolsRelease(project as DBXToolsNodeProject, {
1372
- tagPrefix: options.releaseTagPrefix,
1373
- nodeRelease: options.nodeRelease,
1374
- docs: options.releaseDocs,
1375
- });
1539
+ if (options.releaseMode !== "disabled") {
1540
+ new DBXToolsRelease(project, {
1541
+ tagPrefix: options.releaseTagPrefix,
1542
+ nodeRelease: options.nodeRelease,
1543
+ docs: options.releaseDocs,
1544
+ pythonRoot: options.releasePythonRoot,
1545
+ summary: options.releaseSummary,
1546
+ });
1547
+ }
1376
1548
  }
1377
1549
 
1378
1550
  /**