Skip to content

Commit c0a4205

Browse files
mrgraingithub-actions
and
github-actions
authored
feat: yarn monorepo support (#55)
* feat: support yarn monorepo * feat: codegen for options * chore: self mutation Signed-off-by: github-actions <[email protected]> --------- Signed-off-by: github-actions <[email protected]> Co-authored-by: github-actions <[email protected]>
1 parent 06daa67 commit c0a4205

21 files changed

+18494
-3595
lines changed

Diff for: .gitattributes

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: .github/workflows/pull-request-lint.yml

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: .gitignore

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: .projen/deps.json

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: .projen/files.json

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: .projenrc.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { cdk } from 'projen';
2-
import { MergeQueue } from './src';
2+
import { generateYarnMonorepoOptions } from './projenrc/yarn-monorepo-options';
3+
import { MergeQueue } from './src/merge-queue';
34

45
const project = new cdk.JsiiProject({
56
projenrcTs: true,
@@ -8,19 +9,26 @@ const project = new cdk.JsiiProject({
89
defaultReleaseBranch: 'main',
910
name: 'cdklabs-projen-project-types',
1011
repositoryUrl: 'https://github.com/cdklabs/cdklabs-projen-project-types.git',
12+
devDeps: ['@jsii/spec', 'jsii-reflect'],
1113
deps: ['projen'],
1214
bundledDeps: ['yaml'],
1315
peerDeps: ['projen'],
1416
githubOptions: {
1517
mergify: false,
1618
},
1719
autoApproveUpgrades: true,
18-
autoApproveOptions: { allowedUsernames: ['cdklabs-automation'], secret: 'GITHUB_TOKEN' },
20+
autoApproveOptions: {
21+
allowedUsernames: ['cdklabs-automation'],
22+
secret: 'GITHUB_TOKEN',
23+
},
1924
});
2025

2126
new MergeQueue(project, {
2227
autoMergeOptions: {
2328
secret: 'PROJEN_GITHUB_TOKEN',
2429
},
2530
});
31+
32+
generateYarnMonorepoOptions(project);
33+
2634
project.synth();

Diff for: API.md

+13,639-3,593
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: projenrc/jsii-extend-interface.ts

+224
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { join } from 'path';
2+
import { CollectionKind, Docs, InterfaceType, isCollectionTypeReference, isNamedTypeReference, isPrimitiveTypeReference, isUnionTypeReference, loadAssemblyFromPath, Property, TypeKind, TypeReference } from '@jsii/spec';
3+
import { Assembly, TypeSystem } from 'jsii-reflect';
4+
import { Component, Project, SourceCode, SourceCodeOptions, typescript } from 'projen';
5+
6+
const typesystem = new TypeSystem();
7+
const asm = new Assembly(typesystem, loadAssemblyFromPath('node_modules/projen', false));
8+
typesystem.addAssembly(asm, { isRoot: true });
9+
10+
export interface OptionsGeneratorOptions {
11+
/**
12+
* The interface name
13+
*/
14+
readonly name: string;
15+
/**
16+
* The fqn of the interface
17+
* Is used to auto-add imports.
18+
* Any local referenced types need to adhere to this hierarchy.
19+
*
20+
* @default `${project.name}.${options.name}`
21+
*/
22+
readonly fqn?: string;
23+
/**
24+
* Doc string for the interface
25+
* @default - Interface name
26+
*/
27+
readonly description?: string;
28+
/**
29+
* @default - `${project.srcDir}/${options.name}.ts`
30+
*/
31+
readonly filePath?: string;
32+
/**
33+
* The properties of this interface
34+
* When extending, these will overwrite existing properties.
35+
*
36+
* @default []
37+
*/
38+
readonly properties?: Property[];
39+
/**
40+
* Extends this jsii type
41+
*/
42+
readonly extends?: string;
43+
/**
44+
* Omit these properties from the parent interface
45+
*/
46+
readonly omitProps?: string[];
47+
/**
48+
* Update these properties from the parent interface
49+
*/
50+
readonly updateProps?: {
51+
[name: string]: Partial<Property>;
52+
};
53+
}
54+
55+
export class JsiiInterface extends Component {
56+
public constructor(
57+
project: typescript.TypeScriptProject,
58+
options: OptionsGeneratorOptions,
59+
) {
60+
super(project);
61+
const targetSpec: InterfaceType = {
62+
kind: TypeKind.Interface,
63+
assembly: project.name,
64+
fqn: options.fqn ?? `${project.name}.${options.name}`,
65+
name: options.name,
66+
docs: {
67+
summary: options.description ?? options.name,
68+
},
69+
properties: options.properties || [],
70+
};
71+
72+
if (options.extends) {
73+
const sourceSpec = typesystem.findInterface(options.extends);
74+
const omit = [
75+
...options.omitProps || [],
76+
...options.properties?.map(p => p.name) || [],
77+
];
78+
targetSpec.properties!.push(
79+
...sourceSpec.allProperties
80+
.map(p => {
81+
if (options.updateProps?.[p.name]) {
82+
return {
83+
...p.spec,
84+
...options.updateProps?.[p.name],
85+
};
86+
}
87+
return p.spec;
88+
})
89+
.filter(p => !omit.includes(p.name)),
90+
);
91+
}
92+
93+
targetSpec.properties = targetSpec.properties?.sort((a, b) => a.name.localeCompare(b.name));
94+
95+
const outputFile = options.filePath ?? join(project.srcdir, `${options.name}.ts`);
96+
new InterfaceFile(project, outputFile, targetSpec);
97+
}
98+
}
99+
100+
export class InterfaceFile extends SourceCode {
101+
public constructor(project: Project, filePath: string, private readonly spec: InterfaceType, options: SourceCodeOptions = {}) {
102+
super(project, filePath, options);
103+
104+
this.line(`// ${this.marker}`);
105+
this.renderImports(extractImports(spec));
106+
this.line();
107+
this.renderDocBlock(docsToLines(spec.docs));
108+
this.open(`export interface ${spec.name} {`);
109+
spec.properties?.forEach(p => this.renderProperty(p));
110+
this.close('}');
111+
this.line();
112+
}
113+
114+
protected renderImports(modules: Map<string, Set<string>>) {
115+
Array.from(modules.keys()).sort((a, b) => {
116+
if (a[0] < b[0]) {
117+
return 1;
118+
}
119+
return a.localeCompare(b);
120+
}).forEach(mod => {
121+
const imports = Array.from(modules.get(mod)?.values() || []);
122+
this.line(`import { ${imports.join(', ')} } from '${mod}';`);
123+
});
124+
}
125+
126+
protected renderProperty(p: Property) {
127+
if (p.docs) {
128+
this.renderDocBlock(docsToLines(p.docs));
129+
}
130+
this.line(`readonly ${p.name}${p.optional ? '?' : ''}: ${typeRefToType(p.type, this.spec.fqn)};`);
131+
}
132+
133+
protected renderDocBlock(lines: string[]) {
134+
this.line('/**');
135+
lines.forEach(line => this.line(` * ${line}`));
136+
this.line(' */');
137+
}
138+
}
139+
140+
function docsToLines(docs?: Docs): string[] {
141+
if (!docs) {
142+
return [];
143+
}
144+
145+
const lines = new Array<string>();
146+
147+
if (docs.summary) {
148+
lines.push(docs.summary);
149+
}
150+
if (docs.remarks) {
151+
lines.push(...docs.remarks.split('\n'));
152+
}
153+
if (docs.default) {
154+
lines.push(`@default ${docs.default}`);
155+
}
156+
if (docs.deprecated) {
157+
lines.push(`@deprecated ${docs.deprecated}`);
158+
}
159+
160+
return lines;
161+
}
162+
163+
164+
function typeRefToType(t: TypeReference, containingFqn: string): string {
165+
if (isPrimitiveTypeReference(t)) {
166+
return t.primitive;
167+
}
168+
169+
if (isNamedTypeReference(t)) {
170+
return t.fqn.split('.').slice(1).join('.');
171+
}
172+
173+
if (isCollectionTypeReference(t)) {
174+
switch (t.collection.kind) {
175+
case CollectionKind.Array:
176+
return `Array<${typeRefToType(t.collection.elementtype, containingFqn)}>`;
177+
case CollectionKind.Map:
178+
return `Record<string, ${typeRefToType(t.collection.elementtype, containingFqn)}>`;
179+
default:
180+
return 'any';
181+
}
182+
}
183+
if (isUnionTypeReference(t)) {
184+
return t.union.types.map(ut => typeRefToType(ut, containingFqn)).join(' | ');
185+
}
186+
187+
return 'any';
188+
}
189+
190+
function extractImports(spec: InterfaceType): Map<string, Set<string>> {
191+
const refs = spec.properties?.flatMap(p => collectFQNs(p.type)) || [];
192+
return refs.reduce((mods, ref) => {
193+
const packageName = fqnToImportName(ref, spec.assembly);
194+
const imports = mods.get(packageName) || new Set();
195+
const importName = ref.split('.').slice(1)[0] || ref;
196+
return mods.set(packageName, imports.add(importName));
197+
}, new Map<string, Set<string>>());
198+
}
199+
200+
function fqnToImportName(fqn: string, localAssembly: string): string {
201+
const importName = fqn.split('.', 1)[0];
202+
203+
if (importName === localAssembly) {
204+
return '.'.repeat(fqn.split('.').length - 1) + '/';
205+
}
206+
207+
return importName;
208+
}
209+
210+
function collectFQNs(t: TypeReference): string[] {
211+
if (isNamedTypeReference(t)) {
212+
return [t.fqn];
213+
}
214+
215+
if (isUnionTypeReference(t)) {
216+
return t.union.types.flatMap(collectFQNs);
217+
}
218+
219+
if (isCollectionTypeReference(t)) {
220+
return collectFQNs(t.collection.elementtype);
221+
}
222+
223+
return [];
224+
}

0 commit comments

Comments
 (0)