@langri-sha/projen-project 0.28.0 → 0.30.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/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  Codeowners,
13
13
  type CodeownersOptions,
14
14
  } from '@langri-sha/projen-codeowners'
15
+ import { Dagger, type DaggerOptions } from '@langri-sha/projen-dagger'
15
16
  import {
16
17
  EditorConfig,
17
18
  type EditorConfigOptions,
@@ -37,9 +38,12 @@ import {
37
38
  TypeScriptConfig,
38
39
  type TypeScriptConfigOptions,
39
40
  } from '@langri-sha/projen-typescript-config'
41
+ import { type UvOptions, UvPackage, UvWorkspace } from '@langri-sha/projen-uv'
42
+ import { Worktrunk, type WorktrunkOptions } from '@langri-sha/projen-worktrunk'
40
43
  import {
41
44
  Project as BaseProject,
42
45
  type ProjectOptions as BaseProjectOptions,
46
+ type Dependency,
43
47
  DependencyType,
44
48
  IgnoreFile,
45
49
  IgnoreFileOptions,
@@ -127,6 +131,12 @@ export interface ProjectOptions extends Omit<
127
131
  */
128
132
  codeowners?: CodeownersOptions
129
133
 
134
+ /**
135
+ * Pass in to set up Dagger modules. Root projects only — modules live in
136
+ * top-level directories.
137
+ */
138
+ dagger?: DaggerOptions
139
+
130
140
  /**
131
141
  * EditorConfig options.
132
142
  */
@@ -204,10 +214,22 @@ export interface ProjectOptions extends Omit<
204
214
  */
205
215
  typeScriptConfig?: TypeScriptConfigOptions
206
216
 
217
+ /**
218
+ * Pass in to set up uv. Root projects get a workspace, subprojects a
219
+ * package in it.
220
+ */
221
+ uv?: UvOptions
222
+
207
223
  /*
208
224
  * Whether to use Terrafom.
209
225
  */
210
226
  withTerraform?: boolean
227
+
228
+ /**
229
+ * Configures Worktrunk, when provided. No hooks are supplied by default:
230
+ * each one runs on teammates' machines, so a project declares its own.
231
+ */
232
+ worktrunk?: WorktrunkOptions
211
233
  }
212
234
 
213
235
  export class Project extends BaseProject {
@@ -215,6 +237,7 @@ export class Project extends BaseProject {
215
237
  beachball?: Beachball
216
238
  cargo?: CargoPackage | CargoWorkspace
217
239
  codeowners?: Codeowners
240
+ dagger?: Dagger
218
241
  editorConfig?: EditorConfig
219
242
  eslint?: ESLint
220
243
  husky?: Husky
@@ -231,6 +254,8 @@ export class Project extends BaseProject {
231
254
  renovate?: Renovate
232
255
  swcrc?: SWCConfig
233
256
  typeScriptConfig?: TypeScriptConfig
257
+ uv?: UvPackage | UvWorkspace
258
+ worktrunk?: Worktrunk
234
259
 
235
260
  /**
236
261
  * Packages whose version this preset supplied because the project did not
@@ -272,6 +297,11 @@ export class Project extends BaseProject {
272
297
  this.tasks.tryFind('install:ci')?.reset()
273
298
  }
274
299
 
300
+ // Ahead of Prettier and Renovate, which both read the option: the SDK
301
+ // writes files Prettier must not touch, and the engine version is pinned
302
+ // in the projenrc where only a custom manager can reach it.
303
+ this.#configureDagger(options)
304
+
275
305
  this.#configureESLint(options)
276
306
  this.#configurePrettier(options)
277
307
 
@@ -289,6 +319,8 @@ export class Project extends BaseProject {
289
319
  this.#configureNpmIgnore(options)
290
320
  this.#configurePnpmWorkspace(options)
291
321
  this.#configureReadme(options)
322
+ this.#configureUv(options)
323
+ this.#configureWorktrunk(options)
292
324
  this.#configureRenovate(options)
293
325
  }
294
326
 
@@ -366,9 +398,9 @@ export class Project extends BaseProject {
366
398
  * the bump is reverted by the next synthesis, the pull request merges as a
367
399
  * no-op, and the dependency is proposed again forever.
368
400
  *
369
- * Only `BUILD` declarations are consulted, which is the type `addDevDeps`
370
- * writes; a package the project lists as a runtime dependency is still added
371
- * here, exactly as before.
401
+ * A runtime declaration counts as well. The package is installed either way,
402
+ * and a development pin beside it would only add a second version of it to
403
+ * the manifest.
372
404
  */
373
405
  #addDefaultDevDeps(...specs: string[]) {
374
406
  for (const spec of specs) {
@@ -378,10 +410,12 @@ export class Project extends BaseProject {
378
410
  const name = separator > 0 ? spec.slice(0, separator) : spec
379
411
  const version = separator > 0 ? spec.slice(separator + 1) : '*'
380
412
 
381
- const declared = this.deps.tryGetDependency(name, DependencyType.BUILD)
413
+ const declared =
414
+ this.deps.tryGetDependency(name, DependencyType.BUILD) ??
415
+ this.deps.tryGetDependency(name, DependencyType.RUNTIME)
382
416
 
383
417
  if (declared) {
384
- this.#assertSupported(name, declared.version)
418
+ this.#assertSupported(declared)
385
419
  continue
386
420
  }
387
421
 
@@ -418,7 +452,7 @@ export class Project extends BaseProject {
418
452
  * to resolve rather than naming a version, and `workspace:` is not semver
419
453
  * at all.
420
454
  */
421
- #assertSupported(name: string, declared: string | undefined) {
455
+ #assertSupported({ name, type, version: declared }: Dependency) {
422
456
  const supported = SUPPORTED_VERSIONS[name]
423
457
 
424
458
  if (!declared || !supported || !valid(declared)) {
@@ -429,9 +463,12 @@ export class Project extends BaseProject {
429
463
  return
430
464
  }
431
465
 
466
+ const option =
467
+ type === DependencyType.RUNTIME ? 'package.deps' : 'package.devDeps'
468
+
432
469
  throw new Error(
433
470
  `This project declares ${name}@${declared}, which @langri-sha/projen-project does not support — its peer range is ${supported}.\n\n` +
434
- `Raise the declaration to satisfy ${supported}, or remove it from \`package.devDeps\` and take the version the preset supplies.`,
471
+ `Raise the declaration to satisfy ${supported}, or remove it from \`${option}\` and take the version the preset supplies.`,
435
472
  )
436
473
  }
437
474
 
@@ -447,6 +484,10 @@ export class Project extends BaseProject {
447
484
  },
448
485
  }
449
486
 
487
+ if (!this.parent) {
488
+ this.#addDefaultDevDeps('@babel/core@8.0.5')
489
+ }
490
+
450
491
  this.babel = new Babel(this, deepMerge(defaults, babel))
451
492
 
452
493
  this.typeScriptConfig?.addFile(this.babel.path)
@@ -476,7 +517,7 @@ export class Project extends BaseProject {
476
517
  this.beachball = new Beachball(this, deepMerge(defaults, beachball))
477
518
 
478
519
  this.prettier?.ignore.addPatterns('CHANGELOG.md')
479
- this.#addDefaultDevDeps('beachball@2.65.6')
520
+ this.#addDefaultDevDeps('beachball@2.66.0')
480
521
  this.typeScriptConfig?.addFile(this.beachball!.path)
481
522
  }
482
523
 
@@ -548,6 +589,14 @@ export class Project extends BaseProject {
548
589
  this.codeowners = new Codeowners(this, codeownersOptions)
549
590
  }
550
591
 
592
+ #configureDagger({ dagger }: ProjectOptions) {
593
+ if (!dagger || this.parent) {
594
+ return
595
+ }
596
+
597
+ this.dagger = new Dagger(this, dagger)
598
+ }
599
+
551
600
  #configureEditorConfig({
552
601
  editorConfig: editorConfigOptions,
553
602
  }: ProjectOptions) {
@@ -595,6 +644,10 @@ export class Project extends BaseProject {
595
644
  extends: '@langri-sha/eslint-config',
596
645
  }
597
646
 
647
+ if (!this.parent) {
648
+ this.#addDefaultDevDeps('eslint@10.10.0')
649
+ }
650
+
598
651
  this.eslint = new ESLint(this, deepMerge(defaults, eslint))
599
652
 
600
653
  if (this.projenrc?.filePath) {
@@ -621,6 +674,7 @@ export class Project extends BaseProject {
621
674
  return
622
675
  }
623
676
 
677
+ this.#addDefaultDevDeps('jest@30.5.1')
624
678
  this.jestConfig = new JestConfig(this, jestConfigOptions)
625
679
 
626
680
  this.typeScriptConfig?.addFile(this.jestConfig.path)
@@ -663,6 +717,10 @@ export class Project extends BaseProject {
663
717
  extends: '@langri-sha/lint-staged',
664
718
  }
665
719
 
720
+ if (!this.parent) {
721
+ this.#addDefaultDevDeps('lint-staged@17.5.1')
722
+ }
723
+
666
724
  this.lintStaged = new LintStaged(this, deepMerge(defaults, lintStaged))
667
725
 
668
726
  this.typeScriptConfig?.addFile(this.lintStaged!.path)
@@ -756,7 +814,7 @@ export class Project extends BaseProject {
756
814
  )
757
815
  }
758
816
 
759
- #configurePrettier({ prettier, package: pkg }: ProjectOptions) {
817
+ #configurePrettier({ dagger, prettier, package: pkg }: ProjectOptions) {
760
818
  if (!prettier || this.parent) {
761
819
  return
762
820
  }
@@ -765,9 +823,19 @@ export class Project extends BaseProject {
765
823
  filename:
766
824
  pkg?.type === 'module' ? 'prettier.config.js' : 'prettier.config.mjs',
767
825
  extends: '@langri-sha/prettier',
768
- ignorePatterns: ['.*', 'dist/'],
826
+ ignorePatterns: [
827
+ '.*',
828
+ 'dist/',
829
+ // Everything inside a Dagger module directory is written by the CLI or
830
+ // by the component, each in its own formatting. Prettier would rewrite
831
+ // them and `dagger develop` would put them back, on every run.
832
+ ...(dagger
833
+ ? ['*/dagger.json', '*/package.json', '*/tsconfig.json', '*/sdk/']
834
+ : []),
835
+ ],
769
836
  }
770
837
 
838
+ this.#addDefaultDevDeps('prettier@3.8.3')
771
839
  this.prettier = new Prettier(this, deepMerge(defaults, prettier))
772
840
 
773
841
  if (this.projenrc?.filePath) {
@@ -801,8 +869,10 @@ export class Project extends BaseProject {
801
869
 
802
870
  #configureRenovate({
803
871
  cargo,
872
+ dagger,
804
873
  renovate: renovateOptions,
805
874
  package: pkg,
875
+ uv,
806
876
  }: ProjectOptions) {
807
877
  if (!renovateOptions || this.parent) {
808
878
  return
@@ -864,6 +934,46 @@ export class Project extends BaseProject {
864
934
  },
865
935
  ]
866
936
  : []),
937
+ ...(dagger
938
+ ? [
939
+ {
940
+ description:
941
+ 'Move every Dagger module off one engine release at a time, the way `dagger develop` writes them',
942
+ groupName: 'Dagger engine',
943
+ groupSlug: 'dagger-engine',
944
+ matchDepNames: ['dagger/dagger'],
945
+ },
946
+ {
947
+ description:
948
+ 'The Dagger SDK writes the module manifests, including the TypeScript pin. Upgrades here are reverted by the next `dagger develop`',
949
+ matchManagers: ['npm'],
950
+ matchFileNames: ['*/package.json'],
951
+ enabled: false,
952
+ },
953
+ {
954
+ description:
955
+ 'Track the TypeScript major the Dagger SDK installs into the modules, so `check:types` keeps using the runtime compiler',
956
+ matchManagers: ['npm'],
957
+ matchPackageNames: ['typescript'],
958
+ allowedVersions: '^5',
959
+ },
960
+ ]
961
+ : []),
962
+ // Synthesis writes every requirement into the manifests, so a range
963
+ // Renovate rewrote there would be put back by its next run. Held to
964
+ // the declared ranges, pep621 still moves `uv.lock`, by lock-file
965
+ // updates and maintenance alike; raising a bound is an edit to the
966
+ // projenrc.
967
+ ...(uv
968
+ ? [
969
+ {
970
+ description:
971
+ 'Synthesis writes the pyproject.toml manifests and reverts edits to them. Move only what uv.lock pins, within the ranges they declare',
972
+ matchManagers: ['pep621'],
973
+ rangeStrategy: 'in-range-only' as const,
974
+ },
975
+ ]
976
+ : []),
867
977
  ],
868
978
  customManagers: [
869
979
  {
@@ -926,22 +1036,73 @@ export class Project extends BaseProject {
926
1036
  },
927
1037
  ]
928
1038
  : []),
1039
+ // The engine version is declared in the projenrc and written into
1040
+ // every module manifest from there, so nothing Renovate has a manager
1041
+ // for names it. The `v` prefix sits outside the capture group, and
1042
+ // `extractVersionTemplate` strips it off the release tags, so the
1043
+ // replacement leaves the prefix in place.
1044
+ //
1045
+ // Held to the projenrc by the anchored file pattern below, for the
1046
+ // reason spelled out under the `packageManager` manager: a pattern
1047
+ // that merely contains `projen` reaches the sources of every package
1048
+ // in a monorepo named after it, and `engineVersion:` appears in this
1049
+ // preset's own tests. A projenrc is the only file that declares one.
1050
+ ...(dagger
1051
+ ? [
1052
+ {
1053
+ customType: 'regex' as const,
1054
+ datasourceTemplate: 'github-releases',
1055
+ depNameTemplate: 'dagger/dagger',
1056
+ managerFilePatterns: [
1057
+ '/(^|/)\\.?projenrc\\.(js|cjs|mjs|ts|mts|cts)$/',
1058
+ ],
1059
+ matchStrings: ["engineVersion:\\s*'v(?<currentValue>[^']+)'"],
1060
+ extractVersionTemplate: '^v(?<version>.+)$',
1061
+ },
1062
+ ]
1063
+ : []),
1064
+ // The `packageManager` field is declared in a projenrc and only
1065
+ // reaches the manifest through synthesis, so the npm manager reading
1066
+ // the manifest cannot propose it. Both patterns are held to that one
1067
+ // declaration: an unanchored `pnpm@` also matches the tail of
1068
+ // `langri-sha/github/actions/pnpm@v0.14.1`, and a file pattern that
1069
+ // merely contains `projen` reaches the sources of every package here,
1070
+ // so between them Renovate walked a GitHub Action reference through
1071
+ // four pnpm releases.
929
1072
  {
930
1073
  customType: 'regex',
931
1074
  datasourceTemplate: 'npm',
932
- managerFilePatterns: ['/\\.?projen.*.(js|cjs|mjs|ts|mts|cts)$/'],
933
- matchStrings: ["pnpm@(?<currentValue>[^']+)"],
1075
+ managerFilePatterns: [
1076
+ '/(^|/)\\.?projenrc\\.(js|cjs|mjs|ts|mts|cts)$/',
1077
+ ],
1078
+ matchStrings: [
1079
+ 'packageManager[\'"]?[,:]\\s*[\'"]pnpm@(?<currentValue>[^\'"]+)[\'"]',
1080
+ ],
934
1081
  depNameTemplate: 'pnpm',
935
1082
  depTypeTemplate: 'dependencies',
936
1083
  },
1084
+ // Package executions pinned to a version, which no built-in manager
1085
+ // reads, in the workflows and the projenrcs that spell them out. The
1086
+ // package is whatever the invocation names — forcing `depName` to
1087
+ // `pnpm` offered pnpm's version for every other tool — and the version
1088
+ // ends at the first space or quote.
1089
+ //
1090
+ // Anchored to a projenrc for the reason the two managers above are: a
1091
+ // pattern that merely contains `projen` reaches the sources of every
1092
+ // package here, and this preset's own tests exercise this regex
1093
+ // against a literal `pnpx name@version`. Renovate rewrote the fixture
1094
+ // and left the assertion beside it, failing the suite over a package
1095
+ // nothing depends on.
937
1096
  {
938
1097
  customType: 'regex',
939
1098
  datasourceTemplate: 'npm',
940
- managerFilePatterns: ['/\\.(js|cjs|mjs|ts|mts|cts|ya?ml)$/'],
1099
+ managerFilePatterns: [
1100
+ '/^\\.github/workflows/[^/]+\\.ya?ml$/',
1101
+ '/(^|/)\\.?projenrc\\.(js|cjs|mjs|ts|mts|cts)$/',
1102
+ ],
941
1103
  matchStrings: [
942
- '(bun|p?np)x (?<depName>[\\w\\-\\/]+)@(?<currentValue>[^s]+)',
1104
+ '(bun|p?np)x (?<depName>@?[\\w.-]+(?:/[\\w.-]+)?)@(?<currentValue>[^\\s\'"]+)',
943
1105
  ],
944
- depNameTemplate: 'pnpm',
945
1106
  depTypeTemplate: 'dependencies',
946
1107
  },
947
1108
  ],
@@ -1023,7 +1184,7 @@ export class Project extends BaseProject {
1023
1184
  this.#addDefaultDevDeps('typescript@5.9.3')
1024
1185
 
1025
1186
  if (!swcrc) {
1026
- this.#addDefaultDevDeps('tsx@4.23.12')
1187
+ this.#addDefaultDevDeps('tsx@4.23.13')
1027
1188
  }
1028
1189
  }
1029
1190
 
@@ -1032,6 +1193,48 @@ export class Project extends BaseProject {
1032
1193
  }
1033
1194
  }
1034
1195
 
1196
+ /**
1197
+ * Set up uv, as a workspace at the root and as a member package below.
1198
+ *
1199
+ * A package registers itself as a workspace member on the way, which is the
1200
+ * pairing that otherwise drifts: a subproject added here and a path forgotten
1201
+ * over there.
1202
+ */
1203
+ #configureUv({ uv }: ProjectOptions) {
1204
+ if (!uv) {
1205
+ return
1206
+ }
1207
+
1208
+ // The option is the union of what either component takes, so each branch
1209
+ // has to drop the other's keys.
1210
+ const { pythonVersion, sampleCode, ...pyproject } = uv
1211
+
1212
+ if (!this.parent) {
1213
+ this.uv = new UvWorkspace(this, { ...pyproject, pythonVersion })
1214
+
1215
+ return
1216
+ }
1217
+
1218
+ const workspace =
1219
+ this.root instanceof Project && this.root.uv instanceof UvWorkspace
1220
+ ? this.root.uv
1221
+ : undefined
1222
+
1223
+ this.uv = new UvPackage(this, { ...pyproject, sampleCode })
1224
+
1225
+ workspace?.addMember(
1226
+ path.relative(this.root.outdir, this.outdir).split(path.sep).join('/'),
1227
+ )
1228
+ }
1229
+
1230
+ #configureWorktrunk({ worktrunk }: ProjectOptions) {
1231
+ if (!worktrunk || this.parent) {
1232
+ return
1233
+ }
1234
+
1235
+ this.worktrunk = new Worktrunk(this, worktrunk)
1236
+ }
1237
+
1035
1238
  #populateTypeScriptProjectReferencesFromDependencies() {
1036
1239
  if (this.parent || !this.package || !this.typeScriptConfig) {
1037
1240
  return