@langri-sha/projen-project 0.9.0 → 0.10.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
@@ -1,9 +1,10 @@
1
+ import * as path from 'node:path'
2
+
1
3
  import {
2
4
  Project as BaseProject,
3
5
  type ProjectOptions as BaseProjectOptions,
4
6
  IgnoreFile,
5
7
  IgnoreFileOptions,
6
- YamlFile,
7
8
  javascript,
8
9
  } from 'projen'
9
10
 
@@ -30,8 +31,16 @@ import {
30
31
  import { Beachball, BeachballOptions } from '@langri-sha/projen-beachball'
31
32
  import { License } from '@langri-sha/projen-license'
32
33
 
33
- import { ProjenrcFile } from './lib/index.js'
34
+ import { NodePackage, NodePackageOptions, ProjenrcFile } from './lib/index.js'
34
35
  import { JestConfig, JestConfigOptions } from '@langri-sha/projen-jest-config'
36
+ import { Prettier, PrettierOptions } from '@langri-sha/projen-prettier'
37
+ import { ESLint, ESLintOptions } from '@langri-sha/projen-eslint'
38
+ import { LintStaged, LintStagedOptions } from '@langri-sha/projen-lint-staged'
39
+ import { Babel, BabelOptions } from '@langri-sha/projen-babel'
40
+ import {
41
+ PnpmWorkspace,
42
+ PnpmWorkspaceOptions,
43
+ } from '@langri-sha/projen-pnpm-workspace'
35
44
 
36
45
  export * from '@langri-sha/projen-typescript-config'
37
46
 
@@ -40,32 +49,47 @@ export interface ProjectOptions
40
49
  /*
41
50
  * Pass in to set up Beachball.
42
51
  */
43
- beachballOptions?: BeachballOptions
52
+ babel?: BabelOptions
53
+
54
+ /*
55
+ * Pass in to set up Beachball.
56
+ */
57
+ beachball?: BeachballOptions
44
58
 
45
59
  /*
46
60
  * Pass in to set up Beachball.
47
61
  */
48
- codeownersOptions?: CodeownersOptions
62
+ codeowners?: CodeownersOptions
49
63
 
50
64
  /**
51
65
  * EditorConfig options.
52
66
  */
53
- editorConfigOptions?: EditorConfigOptions
67
+ editorConfig?: EditorConfigOptions
68
+
69
+ /**
70
+ * Pass in to configure ESLint.
71
+ */
72
+ eslint?: ESLintOptions
54
73
 
55
74
  /**
56
75
  * Husky options.
57
76
  */
58
- huskyOptions?: HuskyOptions
77
+ husky?: HuskyOptions
59
78
 
60
79
  /**
61
80
  * Configures Jest, when provided.
62
81
  */
63
- jestConfigOptions?: JestConfigOptions
82
+ jestConfig?: JestConfigOptions
83
+
84
+ /**
85
+ * Configures `lint-staged`, when provided.
86
+ */
87
+ lintStaged?: LintStagedOptions
64
88
 
65
89
  /*
66
90
  * Options for the linting synthesized files.
67
91
  */
68
- lintSynthesizedOptions?: LintSynthesizedOptions
92
+ lintSynthesized?: LintSynthesizedOptions
69
93
 
70
94
  /**
71
95
  * Package configuration options.
@@ -77,43 +101,53 @@ export interface ProjectOptions
77
101
  * @default "Current full year"
78
102
  */
79
103
  copyrightYear?: string
80
- } & javascript.NodePackageOptions
104
+ } & NodePackageOptions
105
+
106
+ /*
107
+ * PNPM workspaces to generate, if provided.
108
+ */
109
+ pnpmWorkspace?: PnpmWorkspaceOptions
110
+
111
+ /**
112
+ * Pass in to configure Prettier.
113
+ */
114
+ prettier?: PrettierOptions
81
115
 
82
116
  /**
83
117
  * Pass in to configure NPM ignore options.
84
118
  */
85
- npmIgnoreOptions?: IgnoreFileOptions
119
+ npmIgnore?: IgnoreFileOptions
86
120
 
87
121
  /*
88
122
  * Pass in to configure Renovate.
89
123
  */
90
- renovateOptions?: RenovateOptions
124
+ renovate?: RenovateOptions
91
125
 
92
126
  /**
93
127
  * TypeScript configuration options.
94
128
  */
95
- typeScriptConfigOptions?: TypeScriptConfigOptions
129
+ typeScriptConfig?: TypeScriptConfigOptions
96
130
 
97
131
  /*
98
132
  * Whether to use Terrafom.
99
133
  */
100
134
  withTerraform?: boolean
101
-
102
- /*
103
- * PNPM workspaces to generate.
104
- */
105
- workspaces?: string[]
106
135
  }
107
136
 
108
137
  export class Project extends BaseProject {
138
+ babel?: Babel
109
139
  beachball?: Beachball
110
140
  codeowners?: Codeowners
111
141
  editorConfig?: EditorConfig
142
+ eslint?: ESLint
112
143
  husky?: Husky
113
144
  jestConfig?: JestConfig
114
145
  license?: License
146
+ lintStaged?: LintStaged
115
147
  npmIgnore?: IgnoreFile
116
- package?: javascript.NodePackage
148
+ package?: NodePackage
149
+ pnpmWorkspace?: PnpmWorkspace
150
+ prettier?: Prettier
117
151
  projenrc?: ProjenrcFile
118
152
  renovate?: Renovate
119
153
  typeScriptConfig?: TypeScriptConfig
@@ -133,9 +167,9 @@ export class Project extends BaseProject {
133
167
  this.tasks.removeTask('pre-compile')
134
168
  this.tasks.removeTask('watch')
135
169
 
136
- this.#configureProjenrc()
137
170
  this.#configurePackage(options)
138
171
  this.#configureTypeScript(options)
172
+ this.#configureProjenrc()
139
173
 
140
174
  if (this.parent) {
141
175
  this.tasks.tryFind('default')?.reset()
@@ -143,61 +177,90 @@ export class Project extends BaseProject {
143
177
  this.tasks.tryFind('install:ci')?.reset()
144
178
  }
145
179
 
180
+ this.#configureESLint(options)
181
+ this.#configurePrettier(options)
182
+
183
+ this.#configureBabel(options)
146
184
  this.#configureBeachball(options)
147
185
  this.#configureCodeowners(options)
148
186
  this.#configureEditorConfig(options)
149
187
  this.#configureHusky(options)
150
188
  this.#configureJestConfig(options)
151
189
  this.#configureLicense(options)
190
+ this.#configureLintStaged(options)
152
191
  this.#configureLintSynthesized(options)
153
192
  this.#configureNpmIgnore(options)
193
+ this.#configurePnpmWorkspace(options)
154
194
  this.#configureRenovate(options)
155
- this.#createPnpmWorkspaces(options)
195
+ }
196
+
197
+ override preSynthesize(): void {
198
+ super.preSynthesize()
199
+
200
+ this.#populateTypeScriptProjectReferencesFromDependencies()
201
+ }
202
+
203
+ get allSubprojects(): BaseProject[] {
204
+ return this.root.node
205
+ .findAll(0)
206
+ .filter((node) => node !== this.root)
207
+ .filter((node): node is BaseProject => node instanceof BaseProject)
208
+ }
209
+
210
+ get allSubprojectsKind(): Project[] {
211
+ return this.allSubprojects.filter(
212
+ (project): project is Project => project instanceof Project,
213
+ )
156
214
  }
157
215
 
158
216
  /**
159
217
  * Add a subproject.
160
218
  */
161
- addSubproject(projectOptions: ProjectOptions) {
162
- return new Project({
219
+ addSubproject(
220
+ projectOptions: ProjectOptions,
221
+ ...compose: Array<(project: Project) => void>
222
+ ) {
223
+ const project = new Project({
163
224
  parent: this,
164
225
  ...projectOptions,
165
226
  })
227
+
228
+ for (const callback of compose) {
229
+ callback(project)
230
+ }
231
+
232
+ return project
166
233
  }
167
234
 
168
235
  /**
169
236
  * Find a project by it's name, e.g. `@acme/some`.
170
237
  */
171
- findSubproject(name: string): Project {
172
- const subprojects: Array<BaseProject> = []
173
-
174
- const addSubproject = (project: BaseProject) => {
175
- for (const subproject of project.subprojects) {
176
- subprojects.push(subproject)
238
+ findSubproject(name: string): Project | undefined {
239
+ return this.allSubprojectsKind.find((project) => project.name === name)
240
+ }
177
241
 
178
- if (subproject.subprojects.length) {
179
- addSubproject(subproject)
180
- }
181
- }
242
+ #configureBabel({ babel }: ProjectOptions) {
243
+ if (!babel) {
244
+ return
182
245
  }
183
246
 
184
- addSubproject(this.root)
185
-
186
- const result = subprojects.find((subproject) => subproject.name === name)
187
-
188
- if (result instanceof Project) {
189
- return result
247
+ const defaults: BabelOptions = {
248
+ options: {
249
+ presets: ['@langri-sha/babel-preset'],
250
+ },
190
251
  }
191
252
 
192
- throw new Error(`Cannot find subproject ${name}`)
253
+ this.babel = new Babel(this, deepMerge(defaults, babel))
254
+
255
+ this.typeScriptConfig?.addFile(this.babel.path)
193
256
  }
194
257
 
195
- #configureBeachball({ beachballOptions }: ProjectOptions) {
196
- if (!beachballOptions || this.parent) {
258
+ #configureBeachball({ beachball }: ProjectOptions) {
259
+ if (!beachball || this.parent) {
197
260
  return
198
261
  }
199
262
 
200
- const options = deepMerge(beachballOptions, {
263
+ const options = deepMerge(beachball, {
201
264
  branch: 'origin/main',
202
265
  gitTags: false,
203
266
  ignorePatterns: [
@@ -211,11 +274,12 @@ export class Project extends BaseProject {
211
274
 
212
275
  this.beachball = new Beachball(this, options)
213
276
 
277
+ this.prettier?.ignore.addPatterns('CHANGELOG.md')
214
278
  this.package?.addDevDeps('beachball@2.43.1')
215
279
  this.typeScriptConfig?.addFile('beachball.config.js')
216
280
  }
217
281
 
218
- #configureCodeowners({ codeownersOptions }: ProjectOptions) {
282
+ #configureCodeowners({ codeowners: codeownersOptions }: ProjectOptions) {
219
283
  if (!codeownersOptions) {
220
284
  return
221
285
  }
@@ -223,7 +287,9 @@ export class Project extends BaseProject {
223
287
  this.codeowners = new Codeowners(this, codeownersOptions)
224
288
  }
225
289
 
226
- #configureEditorConfig({ editorConfigOptions }: ProjectOptions) {
290
+ #configureEditorConfig({
291
+ editorConfig: editorConfigOptions,
292
+ }: ProjectOptions) {
227
293
  if (!editorConfigOptions || this.parent) {
228
294
  return
229
295
  }
@@ -246,9 +312,31 @@ export class Project extends BaseProject {
246
312
  this,
247
313
  deepMerge(editorConfigOptions ?? {}, defaults),
248
314
  )
315
+
316
+ this.prettier?.ignore.addPatterns('!.editorconfig')
317
+ }
318
+
319
+ #configureESLint({ eslint: eslintOptions }: ProjectOptions) {
320
+ if (!eslintOptions) {
321
+ return
322
+ }
323
+
324
+ const defaults: ESLintOptions = {
325
+ filename: 'eslint.config.mjs',
326
+ ignorePatterns: ['.*'],
327
+ extends: '@langri-sha/eslint-config',
328
+ }
329
+
330
+ this.eslint = new ESLint(this, deepMerge(defaults, eslintOptions))
331
+
332
+ if (this.projenrc?.filePath) {
333
+ this.eslint.ignorePatterns.push(`!${this.projenrc.filePath}`)
334
+ }
335
+
336
+ this.typeScriptConfig?.addFile(this.eslint.path)
249
337
  }
250
338
 
251
- #configureHusky({ huskyOptions }: ProjectOptions) {
339
+ #configureHusky({ husky: huskyOptions }: ProjectOptions) {
252
340
  if (!huskyOptions || this.parent) {
253
341
  return
254
342
  }
@@ -260,12 +348,14 @@ export class Project extends BaseProject {
260
348
  this.tryFindObjectFile('package.json')?.addDeletionOverride('pnpm')
261
349
  }
262
350
 
263
- #configureJestConfig({ jestConfigOptions }: ProjectOptions) {
351
+ #configureJestConfig({ jestConfig: jestConfigOptions }: ProjectOptions) {
264
352
  if (!jestConfigOptions || this.parent) {
265
353
  return
266
354
  }
267
355
 
268
356
  this.jestConfig = new JestConfig(this, jestConfigOptions)
357
+
358
+ this.typeScriptConfig?.addFile(this.jestConfig.path)
269
359
  }
270
360
 
271
361
  #configureLicense({ package: pkg }: ProjectOptions) {
@@ -292,33 +382,59 @@ export class Project extends BaseProject {
292
382
  })
293
383
  }
294
384
 
295
- #configureLintSynthesized({ lintSynthesizedOptions }: ProjectOptions) {
296
- new LintSynthesized(
385
+ #configureLintStaged({ lintStaged: lintStagedOptions }: ProjectOptions) {
386
+ if (!lintStagedOptions) {
387
+ return
388
+ }
389
+
390
+ const defaults: LintStagedOptions = {
391
+ filename: 'lint-staged.config.mjs',
392
+ extends: '@langri-sha/lint-staged',
393
+ }
394
+
395
+ this.lintStaged = new LintStaged(
297
396
  this,
298
- lintSynthesizedOptions ?? {
299
- 'package.json': 'pnpx sort-package-json',
300
- '*.{js,jsx,ts,tsx}': 'pnpm eslint --fix',
301
- '*': 'pnpm prettier --write --ignore-unknown',
302
- },
397
+ deepMerge(defaults, lintStagedOptions),
303
398
  )
399
+
400
+ this.typeScriptConfig?.addFile(this.lintStaged!.path)
304
401
  }
305
402
 
306
- #configureNpmIgnore({ jestConfigOptions, npmIgnoreOptions }: ProjectOptions) {
307
- if (!npmIgnoreOptions) {
403
+ #configureLintSynthesized({ lintSynthesized }: ProjectOptions) {
404
+ if (!lintSynthesized) {
405
+ return
406
+ }
407
+
408
+ const defaults: LintSynthesizedOptions = {
409
+ 'package.json': 'pnpx sort-package-json',
410
+ '*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}': 'pnpm eslint --fix',
411
+ '*': 'pnpm prettier --write --ignore-unknown',
412
+ }
413
+
414
+ new LintSynthesized(this, deepMerge(defaults, lintSynthesized))
415
+ }
416
+
417
+ #configureNpmIgnore({
418
+ typeScriptConfig,
419
+ jestConfig,
420
+ npmIgnore,
421
+ }: ProjectOptions) {
422
+ if (!npmIgnore) {
308
423
  return
309
424
  }
310
425
 
311
426
  const defaults: IgnoreFileOptions = {
312
427
  ignorePatterns: [
313
- ...(jestConfigOptions ? ['*.test.*', '.*', '__snapshots__/'] : []),
314
- 'tsconfig*.json',
428
+ '.*',
429
+ ...(jestConfig ? ['*.test.*', '__snapshots__/'] : []),
430
+ ...(typeScriptConfig ? ['tsconfig*.json'] : []),
315
431
  ],
316
432
  }
317
433
 
318
434
  this.npmIgnore = new IgnoreFile(
319
435
  this,
320
436
  '.npmignore',
321
- deepMerge(defaults, npmIgnoreOptions),
437
+ deepMerge(defaults, npmIgnore),
322
438
  )
323
439
  }
324
440
 
@@ -327,17 +443,16 @@ export class Project extends BaseProject {
327
443
  return
328
444
  }
329
445
 
330
- const defaults: javascript.NodePackageOptions = {
446
+ const defaults: NodePackageOptions = {
331
447
  entrypoint: 'src/index.ts',
332
448
  packageManager: javascript.NodePackageManager.PNPM,
333
- minNodeVersion: '20.12.0',
334
449
  }
335
450
 
336
- this.package = new javascript.NodePackage(this, deepMerge(defaults, pkg))
451
+ this.package = new NodePackage(this, deepMerge(defaults, pkg))
337
452
 
338
453
  if (!this.parent) {
339
454
  this.package.addDevDeps('@langri-sha/projen-project@*')
340
- this.package.addDevDeps('projen@0.81.15')
455
+ this.package.addDevDeps('projen@0.82.4')
341
456
  }
342
457
 
343
458
  this.package.removeScript('start')
@@ -348,11 +463,52 @@ export class Project extends BaseProject {
348
463
  }
349
464
  }
350
465
 
466
+ #configurePnpmWorkspace({ pnpmWorkspace }: ProjectOptions) {
467
+ if (!pnpmWorkspace) {
468
+ return
469
+ }
470
+
471
+ this.pnpmWorkspace = new PnpmWorkspace(this, pnpmWorkspace)
472
+
473
+ for (const workspace of pnpmWorkspace.packages ?? []) {
474
+ this.prettier?.ignore.exclude(`${workspace}/lib/`)
475
+ this.eslint?.ignorePatterns.push(`${workspace}/lib/`)
476
+ }
477
+ }
478
+
479
+ #configurePrettier({ prettier: prettierOptions }: ProjectOptions) {
480
+ if (!prettierOptions || this.parent) {
481
+ return
482
+ }
483
+
484
+ const defaults: PrettierOptions = {
485
+ filename: 'prettier.config.mjs',
486
+ extends: '@langri-sha/prettier',
487
+ ignorePatterns: ['.*'],
488
+ }
489
+
490
+ this.prettier = new Prettier(this, deepMerge(defaults, prettierOptions))
491
+
492
+ if (this.projenrc?.filePath) {
493
+ this.prettier.ignore.include(this.projenrc.filePath)
494
+ }
495
+
496
+ if (this.package?.packageManager === javascript.NodePackageManager.PNPM) {
497
+ this.prettier.ignore.addPatterns('pnpm-lock.yaml')
498
+ }
499
+
500
+ this.typeScriptConfig?.addFile(this.prettier!.path)
501
+ }
502
+
351
503
  #configureProjenrc() {
352
504
  this.projenrc = new ProjenrcFile(this, {})
505
+
506
+ if (!this.parent) {
507
+ this.typeScriptConfig?.addFile(this.projenrc.filePath)
508
+ }
353
509
  }
354
510
 
355
- #configureRenovate({ renovateOptions }: ProjectOptions) {
511
+ #configureRenovate({ renovate: renovateOptions }: ProjectOptions) {
356
512
  if (!renovateOptions || this.parent) {
357
513
  return
358
514
  }
@@ -362,7 +518,19 @@ export class Project extends BaseProject {
362
518
  extends: ['config:recommended'],
363
519
  labels: ['dependencies'],
364
520
  reviewersFromCodeOwners: true,
521
+ lockFileMaintenance: {
522
+ enabled: true,
523
+ },
365
524
  customManagers: [
525
+ {
526
+ customType: 'regex',
527
+ datasourceTemplate: 'node',
528
+ depNameTemplate: 'node',
529
+ versioningTemplate: 'node',
530
+ currentValueTemplate: '>= {{currentValue}}',
531
+ fileMatch: ['\\.?projen.*.(js|cjs|mjs|ts|mts|cts)$'],
532
+ matchStrings: ["minNodeVersion:\\s*'(?<currentValue>[^']+)'"],
533
+ },
366
534
  {
367
535
  customType: 'regex',
368
536
  datasourceTemplate: 'npm',
@@ -370,7 +538,7 @@ export class Project extends BaseProject {
370
538
  matchStringsStrategy: 'recursive',
371
539
  matchStrings: [
372
540
  '\\.(?<depType>addDeps|addDevDeps|addPeerDeps)\\([^)]*\\)',
373
- "'(?<depName>[a-zA-Z0-9-]+)@(?<currentValue>[^']+)'",
541
+ "'(?<depName>@?[\\w-\\/]+)@(?<currentValue>[^']+)'",
374
542
  ],
375
543
  depTypeTemplate:
376
544
  "{{#if (equals depType 'addDeps')}}dependencies{{else if (equals depType 'addDevDeps')}}devDependencies{{else}}peerDependencies{{/if}}",
@@ -382,7 +550,7 @@ export class Project extends BaseProject {
382
550
  matchStringsStrategy: 'recursive',
383
551
  matchStrings: [
384
552
  '(?<depType>deps|devDeps|peerDeps):\\s*\\[[^\\]]*\\]',
385
- "'(?<depName>[a-zA-Z0-9-]+)@(?<currentValue>[^']+)'",
553
+ "'(?<depName>@?[\\w-\\/]+)@(?<currentValue>[^']+)'",
386
554
  ],
387
555
  depTypeTemplate:
388
556
  "{{#if (equals depType 'deps')}}dependencies{{else if (equals depType 'devDeps')}}devDependencies{{else}}peerDependencies{{/if}}",
@@ -398,10 +566,18 @@ export class Project extends BaseProject {
398
566
  ],
399
567
  }
400
568
 
569
+ this.addTask('renovate', {
570
+ description: 'Run Renovate locally for debugging',
571
+ exec: 'pnpx renovate --platform=local --repository-cache=reset --dry-run=full',
572
+ })
573
+
401
574
  this.renovate = new Renovate(this, deepMerge(defaults, renovateOptions))
402
575
  }
403
576
 
404
- #configureTypeScript({ parent, typeScriptConfigOptions }: ProjectOptions) {
577
+ #configureTypeScript({
578
+ parent,
579
+ typeScriptConfig: typeScriptConfigOptions,
580
+ }: ProjectOptions) {
405
581
  if (!typeScriptConfigOptions) {
406
582
  return
407
583
  }
@@ -432,29 +608,49 @@ export class Project extends BaseProject {
432
608
  this.package?.addDevDeps('ts-node@10.9.2')
433
609
  }
434
610
 
435
- this.package?.addDevDeps('@langri-sha/tsconfig@*')
611
+ if (this.name !== '@langri-sha/tsconfig') {
612
+ this.package?.addDevDeps('@langri-sha/tsconfig@*')
613
+ }
436
614
  }
437
615
 
438
- #createPnpmWorkspaces({ workspaces }: ProjectOptions) {
439
- if (!workspaces) {
616
+ #populateTypeScriptProjectReferencesFromDependencies() {
617
+ if (this.parent || !this.package || !this.typeScriptConfig) {
440
618
  return
441
619
  }
442
620
 
443
- new YamlFile(this, 'pnpm-workspace.yaml', {
444
- readonly: true,
445
- marker: true,
446
- obj: {
447
- packages: workspaces,
448
- },
449
- })
621
+ const subprojects = this.allSubprojectsKind.filter(
622
+ (project) => project.package && project.typeScriptConfig,
623
+ )
624
+
625
+ const root = path.dirname(this.package!.file.absolutePath)
626
+
627
+ for (const project of subprojects) {
628
+ const from = path.dirname(project.package!.file.absolutePath)
629
+
630
+ if (project.name !== '@langri-sha/tsconfig') {
631
+ this.typeScriptConfig!.addReference(path.relative(root, from))
632
+ }
633
+
634
+ for (const dep of project.deps.all) {
635
+ const reference = subprojects.find(({ name }) => name === dep.name)
636
+
637
+ if (!reference) {
638
+ continue
639
+ }
640
+
641
+ const to = path.dirname(reference.package!.file.absolutePath)
642
+
643
+ project.typeScriptConfig!.addReference(path.relative(from, to))
644
+ }
645
+ }
450
646
  }
451
647
  }
452
648
 
453
649
  const getGitIgnoreOptions = ({
454
650
  gitIgnoreOptions,
455
- huskyOptions,
651
+ husky: huskyOptions,
456
652
  parent,
457
- typeScriptConfigOptions,
653
+ typeScriptConfig: typeScriptConfigOptions,
458
654
  withTerraform,
459
655
  ...options
460
656
  }: ProjectOptions): ProjectOptions['gitIgnoreOptions'] =>
@@ -481,7 +677,8 @@ const getGitIgnoreOptions = ({
481
677
  !.github/
482
678
  ${huskyOptions ? '!.husky/' : ''}
483
679
  !.projen/
484
- ${options.workspaces?.map((workspace) => `${workspace}/lib/`).join('\n') ?? ''}
680
+ dist/
681
+ ${options.pnpmWorkspace?.packages?.map((workspace) => `${workspace}/lib/`).join('\n') ?? ''}
485
682
  node_modules/
486
683
  `
487
684
  .split('\n')
@@ -491,4 +688,11 @@ const getGitIgnoreOptions = ({
491
688
  ],
492
689
  }
493
690
 
494
- const deepMerge = R.mergeDeepWith(R.concat)
691
+ const deepMerge = R.mergeDeepWith(
692
+ R.cond([
693
+ // When both arguments are strings, use the right string.
694
+ [R.allPass([R.is(String), R.is(String)]), R.nthArg(1)],
695
+ // Otherwise, concatenate.
696
+ [R.T, R.concat],
697
+ ]),
698
+ )
package/src/lib/index.ts CHANGED
@@ -1 +1,2 @@
1
+ export * from './node-package.js'
1
2
  export * from './projenrc.js'
@@ -0,0 +1,50 @@
1
+ import * as fs from 'node:fs'
2
+ import { Project, javascript } from 'projen'
3
+
4
+ /**
5
+ * Options for configuring Node.js packages.
6
+ */
7
+ export interface NodePackageOptions extends javascript.NodePackageOptions {
8
+ peerDependenciesMeta?: Record<string, { optional?: boolean }>
9
+ type?: 'commonjs' | 'module'
10
+ }
11
+
12
+ /**
13
+ * Custom Node.js package manager that preserves package.
14
+ */
15
+ export class NodePackage extends javascript.NodePackage {
16
+ constructor(project: Project, options?: NodePackageOptions) {
17
+ super(project, options)
18
+
19
+ this.addVersion(this.#getPackageJson()?.version ?? '0.0.0')
20
+
21
+ if (options?.type) {
22
+ this.addField('type', options.type)
23
+ }
24
+
25
+ if (options?.peerDependenciesMeta) {
26
+ this.addField('peerDependenciesMeta', options.peerDependenciesMeta)
27
+ }
28
+
29
+ // Reset, so we can run `pnpx projen` on CI after Renovate upgrades.
30
+ this.project.tasks
31
+ .tryFind('install:ci')
32
+ ?.reset(this.project.tasks.tryFind('install')?.steps[0]?.exec)
33
+ }
34
+
35
+ #getPackageJson() {
36
+ try {
37
+ return JSON.parse(fs.readFileSync(this.file.absolutePath, 'utf-8'))
38
+ } catch (_) {
39
+ return undefined
40
+ }
41
+ }
42
+
43
+ override postSynthesize(): void {
44
+ if (this.project.parent) {
45
+ return
46
+ }
47
+
48
+ super.postSynthesize()
49
+ }
50
+ }