@dbx-tools/projen 0.6.222 → 0.6.224

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,7 +35,6 @@ import {
33
35
  type DiscoveredPackage,
34
36
  projectName,
35
37
  readPackageManifest,
36
- repoRoot,
37
38
  scanPackages,
38
39
  toPosix,
39
40
  } from "./packages.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,152 @@ 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
+ /** Set to `false` to omit normal npm workspace publication. */
625
+ readonly nodeRelease?: boolean;
626
+ /** Unified dbx-tools release workflow, or no release surface. Defaults to `dbx-tools`. */
627
+ readonly releaseMode?: DBXToolsReleaseMode;
628
+ /** Prefix for generated release tags. Defaults to `v`. */
629
+ readonly releaseTagPrefix?: string;
630
+ /** Semantic PR title validation policy. Omitted or `false` disables title validation. */
631
+ readonly pullRequestTitlePolicy?: false | PullRequestTitlePolicyOptions;
632
+ /** Repository output paths excluded from generated workflow dependency-cache hashing. */
633
+ readonly workflowCacheIgnorePaths?: readonly string[];
634
+ /**
635
+ * Extra workspace member paths (repo-relative, POSIX) to list in the workspace
636
+ * config ALONGSIDE the discovered `packageRoots` members - for a package that
637
+ * is synthesized by its OWN `.projenrc.ts` (so it isn't a root subproject) but
638
+ * should still resolve as a workspace sibling. The `@dbx-tools/projen` engine in
639
+ * `projen/` is the case: it synthesizes itself (avoiding a dogfooding cycle) yet
640
+ * is a member of the single bun workspace, so the root links it from source.
641
+ */
642
+ readonly extraWorkspaceMembers?: readonly string[];
643
+ /**
644
+ * Install workspace dependencies once from the custom root instead of once
645
+ * per child project during post-synthesis. Defaults to `true`; set `false` to
646
+ * preserve projen's native per-project install tasks.
647
+ */
648
+ readonly rootInstallOnly?: boolean;
649
+ };
650
+
588
651
  /** 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
- }
652
+ export type DBXToolsTypeScriptProjectOptions = Partial<
653
+ Omit<
654
+ typescript.TypeScriptProjectOptions,
655
+ keyof ReleaseProjectOptions | "release" | "releaseToNpm"
656
+ >
657
+ > &
658
+ DBXToolsJavaScriptProjectOptions & {
659
+ /** Emit the projen-owned bun app scaffolding (`bunfig.toml`/`dev.ts`/`build.ts`). */
660
+ readonly bunApp?: boolean;
661
+ };
594
662
 
595
663
  /**
596
664
  * A monorepo root. Scans `packageRoots` and appends a
@@ -609,14 +677,12 @@ export class DBXToolsNodeProject
609
677
  vsCode?: DBXToolsVsCode;
610
678
  readonly extraWorkspaceMembers: readonly string[];
611
679
  readonly releaseBranch: string;
680
+ readonly workflowCacheIgnorePaths: readonly string[];
612
681
  private readonly rootInstallOnly: boolean;
613
682
 
614
683
  constructor(options: DBXToolsJavaScriptProjectOptions = {}) {
684
+ validateReleaseOptions(options);
615
685
  const { name, scope } = resolveIdentity(options);
616
- const releaseDefaults =
617
- options.release && options.releaseTrigger === undefined
618
- ? { releaseTrigger: ReleaseTrigger.tagged({ tags: ["v*"] }) }
619
- : {};
620
686
  // Holds the workspace state (members/catalog/allowBuilds/overrides). Under
621
687
  // bun, projen's base constructor does NOT create the `PnpmWorkspaceYaml`
622
688
  // component (its `configurePnpm` call site is gated to pnpm), so this state's
@@ -626,7 +692,6 @@ export class DBXToolsNodeProject
626
692
  const pnpmWorkspace = new PnpmWorkspaceState(options);
627
693
  super({
628
694
  ...defaultProjectOptions(options),
629
- ...releaseDefaults,
630
695
  pnpmOptions: {
631
696
  ...options.pnpmOptions,
632
697
  workspaceYamlOptions: pnpmWorkspace.options,
@@ -645,8 +710,10 @@ export class DBXToolsNodeProject
645
710
  this.scope = scope;
646
711
  this.extraWorkspaceMembers = options.extraWorkspaceMembers ?? [];
647
712
  this.releaseBranch = options.defaultReleaseBranch ?? "main";
713
+ this.workflowCacheIgnorePaths = options.workflowCacheIgnorePaths ?? [];
648
714
  this.rootInstallOnly = options.rootInstallOnly !== false;
649
715
  this.dbxToolsConfig = new DBXToolsConfig(this, options);
716
+ configureBuildWorkflow(this, options);
650
717
  initProject(this, options);
651
718
  }
652
719
 
@@ -654,7 +721,9 @@ export class DBXToolsNodeProject
654
721
  const steps = super.renderWorkflowSetup(options);
655
722
  if (this.parent) return steps;
656
723
  return steps.flatMap((step) => {
657
- if (step.uses === "oven-sh/setup-bun@v2") return [...bunCacheRestoreSteps(this)];
724
+ if (step.uses === "oven-sh/setup-bun@v2") {
725
+ return [...bunCacheRestoreSteps(this, { ignorePaths: this.workflowCacheIgnorePaths })];
726
+ }
658
727
  if (step.run === "bun install") {
659
728
  return [
660
729
  step,
@@ -669,16 +738,9 @@ export class DBXToolsNodeProject
669
738
  }
670
739
 
671
740
  public override preSynthesize(): void {
672
- if (this.rootInstallOnly) this.with(ROOT_INSTALL_ONLY_MIXIN);
741
+ prepareRootSynthesis(this, this.rootInstallOnly);
673
742
  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);
743
+ resolveRootWorkspace(this, this.extraWorkspaceMembers);
682
744
  preSynthesizeProject(this);
683
745
  }
684
746
  }
@@ -726,10 +788,16 @@ export class DBXToolsTypeScriptProject
726
788
  pnpmWorkspace?: PnpmWorkspaceState;
727
789
  rootTsconfig?: DBXToolsRootTsconfig;
728
790
  vsCode?: DBXToolsVsCode;
791
+ readonly extraWorkspaceMembers: readonly string[];
792
+ readonly releaseBranch: string;
793
+ readonly workflowCacheIgnorePaths: readonly string[];
794
+ private readonly rootInstallOnly: boolean;
729
795
 
730
796
  constructor(options: DBXToolsTypeScriptProjectOptions) {
797
+ validateReleaseOptions(options);
731
798
  const { name, scope } = resolveIdentity(options);
732
799
  const parent = options?.parent;
800
+ const pnpmWorkspace = parent ? undefined : new PnpmWorkspaceState(options);
733
801
  const packageManager =
734
802
  options.packageManager ??
735
803
  inheritedPackageManager(parent instanceof javascript.NodeProject ? parent : undefined);
@@ -738,6 +806,14 @@ export class DBXToolsTypeScriptProject
738
806
  ...defaultTypeScriptProjectOptions(options),
739
807
  name: options.name ?? name,
740
808
  packageManager,
809
+ ...(pnpmWorkspace
810
+ ? {
811
+ pnpmOptions: {
812
+ ...options.pnpmOptions,
813
+ workspaceYamlOptions: pnpmWorkspace.options,
814
+ },
815
+ }
816
+ : {}),
741
817
  tsconfig: {
742
818
  ...options.tsconfig,
743
819
  include: options.tsconfig?.include,
@@ -750,7 +826,13 @@ export class DBXToolsTypeScriptProject
750
826
  },
751
827
  },
752
828
  });
829
+ this.pnpmWorkspace = pnpmWorkspace;
830
+ pnpmWorkspace?.attachWorkspaceFile(this);
753
831
  this.scope = scope;
832
+ this.extraWorkspaceMembers = options.extraWorkspaceMembers ?? [];
833
+ this.releaseBranch = options.defaultReleaseBranch ?? "main";
834
+ this.workflowCacheIgnorePaths = options.workflowCacheIgnorePaths ?? [];
835
+ this.rootInstallOnly = options.rootInstallOnly !== false;
754
836
  // Pairs with `jsx` in SHARED_COMPILER_OPTIONS: projen's default `include` is
755
837
  // `src/**/*.ts` only, which silently omits a `.tsx` file from the program
756
838
  // instead of failing, so authoring a React component would otherwise need
@@ -772,27 +854,106 @@ export class DBXToolsTypeScriptProject
772
854
  this.package.addField("version", readWorkspaceVersion(this.root.outdir));
773
855
  addPackageFiles(this, "index.ts", "src");
774
856
  // `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
- });
857
+ // runs it with bun's own fast runner. The native no-tests option is portable
858
+ // across Windows and Unix and lets packages without tests remain a no-op.
859
+ this.testTask.exec("bun test test --pass-with-no-tests");
782
860
  if (options.bunApp ?? false) {
783
861
  new BunfigFile(this);
784
862
  new BunDevServerFile(this);
785
863
  new BunBuildFile(this);
786
864
  }
865
+ configureBuildWorkflow(this, options);
787
866
  initProject(this, options);
788
867
  }
789
868
 
790
869
  public override preSynthesize(): void {
870
+ prepareRootSynthesis(this, this.rootInstallOnly);
791
871
  super.preSynthesize();
872
+ resolveRootWorkspace(this, this.extraWorkspaceMembers);
792
873
  preSynthesizeProject(this);
793
874
  }
794
875
  }
795
876
 
877
+ function configureBuildWorkflow(
878
+ project: DBXToolsNodeProject | DBXToolsTypeScriptProject,
879
+ options: DBXToolsJavaScriptProjectOptions,
880
+ ): void {
881
+ if (project.parent || !options.buildWorkflow || !project.github) return;
882
+ const validation = project.addTask("pr:validate", {
883
+ description: "Validate generated files and TypeScript packages",
884
+ });
885
+ validation.exec("bunx projen default");
886
+ validation.exec("bun run compile");
887
+ const configured = options.buildWorkflowOptions ?? {};
888
+ const compatibility = nodeWorkflowCompatibility(project);
889
+ const configuredRunner = configured.runsOn !== undefined || configured.runsOnGroup !== undefined;
890
+ const workflow = new BuildWorkflow(project, {
891
+ buildTask: validation,
892
+ artifactsDirectory: project.artifactsDirectory,
893
+ containerImage: options.workflowContainerImage,
894
+ gitIdentity: options.workflowGitIdentity,
895
+ permissions: { idToken: compatibility.determineIdTokenPermissions(options) },
896
+ ...configured,
897
+ preBuildSteps: [
898
+ ...project.renderWorkflowSetup({
899
+ installStepConfiguration: {
900
+ workingDirectory: compatibility.determineInstallWorkingDirectory(),
901
+ },
902
+ mutable: configured.mutableInstall ?? configured.mutableBuild ?? true,
903
+ }),
904
+ ...(configured.preBuildSteps ?? []),
905
+ ],
906
+ postBuildSteps: [...(options.postBuildSteps ?? [])],
907
+ runsOn: configuredRunner ? configured.runsOn : options.workflowRunsOn,
908
+ runsOnGroup: configuredRunner ? configured.runsOnGroup : options.workflowRunsOnGroup,
909
+ });
910
+ workflow.addPostBuildSteps(...compatibility.renderUploadCoverageJobStep(options));
911
+ compatibility.buildWorkflow = workflow;
912
+
913
+ if ((options.autoMerge ?? true) && project.github.mergify) {
914
+ const autoMerge = new AutoMerge(project.github, options.autoMergeOptions);
915
+ autoMerge.addConditionsLater({
916
+ render: () => workflow.buildJobIds.map((id) => `status-success=${id}`),
917
+ });
918
+ compatibility.autoMerge = autoMerge;
919
+ }
920
+ }
921
+
922
+ interface NodeWorkflowCompatibility {
923
+ autoMerge?: AutoMerge;
924
+ buildWorkflow?: BuildWorkflow;
925
+ determineIdTokenPermissions(options: DBXToolsJavaScriptProjectOptions): JobPermission | undefined;
926
+ determineInstallWorkingDirectory(): string | undefined;
927
+ renderUploadCoverageJobStep(options: DBXToolsJavaScriptProjectOptions): JobStep[];
928
+ }
929
+
930
+ /**
931
+ * Access the narrow NodeProject workflow hooks that have no public equivalent.
932
+ * BuildWorkflow itself is created exclusively through its public constructor.
933
+ */
934
+ function nodeWorkflowCompatibility(project: javascript.NodeProject): NodeWorkflowCompatibility {
935
+ return project as unknown as NodeWorkflowCompatibility;
936
+ }
937
+
938
+ function prepareRootSynthesis(
939
+ project: DBXToolsNodeProject | DBXToolsTypeScriptProject,
940
+ rootInstallOnly: boolean,
941
+ ): void {
942
+ if (!project.parent && rootInstallOnly) project.with(ROOT_INSTALL_ONLY_MIXIN);
943
+ }
944
+
945
+ function resolveRootWorkspace(
946
+ project: DBXToolsNodeProject | DBXToolsTypeScriptProject,
947
+ extraWorkspaceMembers: readonly string[],
948
+ ): void {
949
+ if (project.parent) return;
950
+ const version = readWorkspaceVersion(project.outdir);
951
+ for (const member of extraWorkspaceMembers) {
952
+ syncWorkspaceManifestVersion(join(project.outdir, member, "package.json"), version);
953
+ }
954
+ project.pnpmWorkspace?.resolveMembers(project, extraWorkspaceMembers);
955
+ }
956
+
796
957
  /**
797
958
  * Regenerates the repo's generated source after synth: first the codegen
798
959
  * modules (ts-to-zod schemas from each `codegen`-declaring package's upstream
@@ -808,8 +969,10 @@ export class DBXToolsTypeScriptProject
808
969
  */
809
970
  class GeneratedSource extends Component {
810
971
  public override postSynthesize(): void {
811
- generateCodegen();
812
- generateBarrels();
972
+ const projectRoot = this.project.outdir;
973
+ const includeRoot = this.project instanceof DBXToolsTypeScriptProject;
974
+ generateCodegen(projectRoot, { includeRoot });
975
+ generateBarrels({ projectRoot, includeRoot });
813
976
  }
814
977
  }
815
978
 
@@ -837,7 +1000,10 @@ class WorkspaceValidationTasks extends Component {
837
1000
  public override preSynthesize(): void {
838
1001
  if (this.configured) return;
839
1002
  this.configured = true;
840
- const project = this.project as javascript.NodeProject;
1003
+ const project = this.project as DBXToolsNodeProject | DBXToolsTypeScriptProject;
1004
+ if (project.subprojects.length === 0 && project.extraWorkspaceMembers.length === 0) {
1005
+ return;
1006
+ }
841
1007
  for (const task of [project.compileTask, project.testTask]) {
842
1008
  task.exec(`bun run --filter '*' ${task.name}`);
843
1009
  }
@@ -852,29 +1018,17 @@ class WorkspaceValidationTasks extends Component {
852
1018
  * off) do not gain new files.
853
1019
  */
854
1020
  class WorkflowDefaults extends Component {
1021
+ constructor(
1022
+ project: javascript.NodeProject,
1023
+ private readonly pullRequestTitlePolicy: false | PullRequestTitlePolicyOptions | undefined,
1024
+ ) {
1025
+ super(project);
1026
+ }
1027
+
855
1028
  public override preSynthesize(): void {
856
1029
  const project = this.project as javascript.NodeProject;
857
1030
  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
- }
1031
+ if (workflow && this.pullRequestTitlePolicy) {
878
1032
  workflow.addJob("pr-title", {
879
1033
  name: "Validate PR title",
880
1034
  runsOn: ["ubuntu-latest"],
@@ -886,8 +1040,8 @@ class WorkflowDefaults extends Component {
886
1040
  uses: "amannn/action-semantic-pull-request@v6",
887
1041
  env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" },
888
1042
  with: {
889
- types: ["feat", "fix", "chore"].join("\n"),
890
- requireScope: false,
1043
+ types: this.pullRequestTitlePolicy.types.join("\n"),
1044
+ requireScope: this.pullRequestTitlePolicy.requireScope ?? false,
891
1045
  },
892
1046
  },
893
1047
  ],
@@ -995,11 +1149,19 @@ function packageNameFor(scope: string, relPath: string, omitPrefixes: string[]):
995
1149
  * either way it is parsed through {@link PackageIdentifier} so a scoped value
996
1150
  * (`@dbx-tools` or a full `@dbx-tools/root` name) yields the bare scope `dbx-tools`.
997
1151
  */
998
- function resolveIdentity(options: { name?: string; scope?: string }): {
1152
+ function resolveIdentity(options: {
1153
+ name?: string;
1154
+ scope?: string;
1155
+ outdir?: string;
1156
+ parent?: Project;
1157
+ }): {
999
1158
  name: string;
1000
1159
  scope: string;
1001
1160
  } {
1002
- const name = options.name && options.name.length ? options.name : projectName();
1161
+ const projectRoot = options.parent
1162
+ ? resolve(options.parent.outdir, options.outdir ?? ".")
1163
+ : resolve(options.outdir ?? process.cwd());
1164
+ const name = options.name && options.name.length ? options.name : projectName(projectRoot);
1003
1165
  const rawScope = options.scope && options.scope.length ? options.scope : name;
1004
1166
  const identifier = PackageIdentifier.parse(rawScope);
1005
1167
  return { name, scope: identifier?.scope ?? identifier?.name ?? rawScope };
@@ -1227,9 +1389,7 @@ function initProject(
1227
1389
  project.annotateGenerated(`/${root}/**/index.ts`);
1228
1390
  project.annotateGenerated(`/${root}/openapi/**`);
1229
1391
  }
1230
- const extraWorkspaceMembers =
1231
- project instanceof DBXToolsNodeProject ? project.extraWorkspaceMembers : [];
1232
- for (const member of extraWorkspaceMembers) {
1392
+ for (const member of project.extraWorkspaceMembers) {
1233
1393
  project.annotateGenerated(`/${member}/index.ts`);
1234
1394
  }
1235
1395
 
@@ -1241,13 +1401,17 @@ function initProject(
1241
1401
  // repairs the worktree it is meant to validate; `eslint:fix` is the explicit
1242
1402
  // local mutation path.
1243
1403
  const eslint = new javascript.Eslint(project, {
1244
- dirs: [...roots, "projen"],
1404
+ dirs: [...roots, ...project.extraWorkspaceMembers],
1245
1405
  fileExtensions: [".ts", ".tsx"],
1246
1406
  projectService: true,
1247
1407
  prettier: Boolean(project.prettier),
1248
- tsconfigPath: "./tsconfig.json",
1408
+ tsconfigPath:
1409
+ project instanceof typescript.TypeScriptProject
1410
+ ? (project.tsconfig?.fileName ?? "./tsconfig.json")
1411
+ : "./tsconfig.json",
1249
1412
  commandOptions: { fix: false },
1250
1413
  });
1414
+ eslint.allowDefaultProjectFiles(".projenrc.ts");
1251
1415
  project.addTask("eslint:fix", {
1252
1416
  description: "Fix ESLint issues across the codebase",
1253
1417
  exec: "bun run eslint -- --fix",
@@ -1260,7 +1424,9 @@ function initProject(
1260
1424
  eslint.addIgnorePattern(`${root}/**/index.ts`);
1261
1425
  }
1262
1426
  eslint.addIgnorePattern("**/src/generated/**");
1263
- eslint.addIgnorePattern("projen/index.ts");
1427
+ for (const member of project.extraWorkspaceMembers) {
1428
+ eslint.addIgnorePattern(`${member}/index.ts`);
1429
+ }
1264
1430
  // The generated bun app scripts + unmanaged overrides live at the package root,
1265
1431
  // outside any `src/**` tsconfig include, so the type-aware parser cannot resolve
1266
1432
  // them to a project. ESLint still cannot parse them.
@@ -1362,17 +1528,20 @@ function initProject(
1362
1528
  }
1363
1529
 
1364
1530
  new WorkspaceValidationTasks(project);
1365
- new WorkflowDefaults(project);
1531
+ new WorkflowDefaults(project, options.pullRequestTitlePolicy);
1366
1532
  new PrettierIgnoreGenerated(project);
1367
1533
 
1368
1534
  new GeneratedSource(project);
1369
1535
  // Version mutation, reviewed release preparation, and public publication are
1370
1536
  // 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
- });
1537
+ if (options.releaseMode !== "disabled") {
1538
+ new DBXToolsRelease(project, {
1539
+ tagPrefix: options.releaseTagPrefix,
1540
+ nodeRelease: options.nodeRelease,
1541
+ docs: options.releaseDocs,
1542
+ pythonRoot: options.releasePythonRoot,
1543
+ });
1544
+ }
1376
1545
  }
1377
1546
 
1378
1547
  /**