Skip to content

Commit cf5ae58

Browse files
authored
Merge pull request #3520 from hey-api/chore/renderer-exports
chore: render exports
2 parents 234c7f4 + aad90e4 commit cf5ae58

5 files changed

Lines changed: 165 additions & 111 deletions

File tree

packages/openapi-python/src/plugins/@hey-api/sdk/v1/node.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export function createShell(plugin: HeyApiSdkPlugin['Instance']): StructureShell
100100

101101
const symbolClient = plugin.external('client.Client');
102102

103-
const c = $.class(symbol).extends(symbolClient);
103+
const c = $.class(symbol).export().extends(symbolClient);
104104

105105
const dependencies: Array<ReturnType<typeof $.class>> = [];
106106

@@ -137,8 +137,7 @@ export function toNode(
137137
String(item.location[item.location.length - 1]),
138138
plugin.config.operations.methodName,
139139
);
140-
const node = $.func(fnName);
141-
node.do($.return($.id('None')));
140+
const node = $.func(fnName).export().do($('None').return());
142141
nodes.push(node);
143142
}
144143
return { nodes };

packages/openapi-python/src/py-dsl/decl/class.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import { NewlinePyDsl } from '../layout/newline';
77
import { DecoratorMixin } from '../mixins/decorator';
88
import { DocMixin } from '../mixins/doc';
99
import { LayoutMixin } from '../mixins/layout';
10+
import { ExportMixin } from '../mixins/modifiers';
1011
import { safeRuntimeName } from '../utils/name';
1112

1213
type Body = Array<MaybePyDsl<py.Statement>>;
1314

14-
const Mixed = DecoratorMixin(DocMixin(LayoutMixin(PyDsl<py.ClassDeclaration>)));
15+
const Mixed = DecoratorMixin(DocMixin(ExportMixin(LayoutMixin(PyDsl<py.ClassDeclaration>))));
1516

1617
export class ClassPyDsl extends Mixed {
1718
readonly '~dsl' = 'ClassPyDsl';

packages/openapi-python/src/py-dsl/decl/func.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import { DecoratorMixin } from '../mixins/decorator';
77
import { DoMixin } from '../mixins/do';
88
import { DocMixin } from '../mixins/doc';
99
import { LayoutMixin } from '../mixins/layout';
10-
import { AsyncMixin, ModifiersMixin } from '../mixins/modifiers';
10+
import { AsyncMixin, ExportMixin } from '../mixins/modifiers';
1111
import { safeRuntimeName } from '../utils/name';
1212

13-
const Mixed = DecoratorMixin(
14-
DocMixin(DoMixin(LayoutMixin(AsyncMixin(ModifiersMixin(PyDsl<py.FunctionDeclaration>))))),
13+
const Mixed = AsyncMixin(
14+
DecoratorMixin(DocMixin(DoMixin(ExportMixin(LayoutMixin(PyDsl<py.FunctionDeclaration>))))),
1515
);
1616

1717
export class FuncPyDsl extends Mixed {

packages/openapi-python/src/py-dsl/mixins/modifiers.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function modifierToKind(modifier: Modifier): py.Expression {
3434
}
3535
}
3636

37-
export function ModifiersMixin<T extends py.Node, TBase extends BaseCtor<T>>(Base: TBase) {
37+
function ModifiersMixin<T extends py.Node, TBase extends BaseCtor<T>>(Base: TBase) {
3838
abstract class Modifiers extends Base {
3939
protected modifiers: Array<py.Expression> = [];
4040

@@ -84,3 +84,38 @@ export function AsyncMixin<T extends py.Node, TBase extends BaseCtor<T>>(Base: T
8484

8585
return Async as unknown as MixinCtor<TBase, AsyncMethods>;
8686
}
87+
88+
export interface ExportMethods extends Modifiers {
89+
/**
90+
* Adds the `export` keyword modifier if the condition is true.
91+
*
92+
* @param condition - Whether to add the modifier (default: true).
93+
* @returns The target object for chaining.
94+
*/
95+
export(condition?: boolean): this;
96+
}
97+
98+
/**
99+
* Mixin that adds an `export` modifier to a node.
100+
*/
101+
export function ExportMixin<T extends py.Node, TBase extends BaseCtor<T>>(Base: TBase) {
102+
const Mixed = ModifiersMixin(Base as BaseCtor<T>);
103+
104+
abstract class Export extends Mixed {
105+
/**
106+
* Adds the `export` keyword modifier if the condition is true.
107+
*
108+
* @param condition - Whether to add the modifier (default: true).
109+
* @returns The target object for chaining.
110+
*/
111+
protected export(condition?: boolean): this {
112+
const cond = arguments.length === 0 ? true : Boolean(condition);
113+
this.exported = cond;
114+
// TODO: remove this side-effect once planner handles exported flag
115+
if (this.symbol) this.symbol.setExported(cond);
116+
return this;
117+
}
118+
}
119+
120+
return Export as unknown as MixinCtor<TBase, ExportMethods>;
121+
}

packages/openapi-python/src/py-dsl/utils/render.ts

Lines changed: 122 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type ExportsOptions = {
1111
preferExportAll?: boolean;
1212
};
1313
type Header = MaybeArray<string> | null | undefined;
14-
type Imports = ReadonlyArray<ReadonlyArray<ModuleImport>>;
14+
type Imports = Array<ReadonlyArray<ModuleImport>>;
1515

1616
function headerToLines(header: Header): ReadonlyArray<string> {
1717
if (!header) return [];
@@ -69,7 +69,7 @@ export class PythonRenderer implements Renderer {
6969
render(ctx: RenderContext<PyDsl>): string {
7070
const header = typeof this._header === 'function' ? this._header(ctx) : this._header;
7171
return PythonRenderer.astToString({
72-
// exports: this.getExports(ctx),
72+
exports: this.getExports(ctx),
7373
exportsOptions: {
7474
preferExportAll: this._preferExportAll,
7575
},
@@ -101,31 +101,62 @@ export class PythonRenderer implements Renderer {
101101
text += `${header}\n`;
102102
}
103103

104+
const argsImports = args.imports ?? [];
105+
106+
for (const group of args.exports ?? []) {
107+
for (const exp of group) {
108+
let found = false;
109+
for (const impGroup of argsImports) {
110+
if (found) break;
111+
for (const imp of impGroup) {
112+
if (imp.modulePath === exp.modulePath) {
113+
// TODO: merge imports and exports from the same module
114+
found = true;
115+
break;
116+
}
117+
}
118+
}
119+
if (!found) {
120+
argsImports.push([
121+
{
122+
imports: exp.exports.map((exp) => ({
123+
isTypeOnly: exp.isTypeOnly,
124+
localName: exp.exportedName,
125+
sourceName: exp.exportedName,
126+
})),
127+
isTypeOnly: false,
128+
kind: 'named',
129+
modulePath: exp.modulePath,
130+
},
131+
]);
132+
}
133+
}
134+
}
135+
104136
let imports = '';
105-
for (const group of args.imports ?? []) {
137+
for (const group of argsImports) {
106138
if (imports) imports += '\n';
107139
for (const imp of group) {
108-
imports += `${astToString(PythonRenderer.toImportAst(imp))}\n`;
140+
imports += `${astToString(PythonRenderer.toImportAst(imp))}`;
109141
}
110142
}
111143
text = `${text}${text && imports ? '\n' : ''}${imports}`;
112144

145+
let exports = '';
146+
for (const group of args.exports ?? []) {
147+
if (exports) exports += '\n';
148+
for (const exp of group) {
149+
exports += `${astToString(PythonRenderer.toExportAst(exp, args.exportsOptions))}`;
150+
}
151+
}
152+
text = `${text}${text && exports ? '\n' : ''}${exports}`;
153+
113154
let nodes = '';
114155
for (const node of args.nodes ?? []) {
115-
if (nodes) nodes += '\n';
116-
nodes += `${astToString(node.toAst())}\n`;
156+
if (nodes) nodes += '\n\n';
157+
nodes += `${astToString(node.toAst())}`;
117158
}
118-
text = `${text}${text && nodes ? '\n' : ''}${nodes}`;
119-
120-
const exports = '';
121-
// let exports = '';
122-
// for (const group of args.exports ?? []) {
123-
// if ((!exports && nodes) || exports) exports += '\n';
124-
// for (const exp of group) {
125-
// exports += `${astToString(PythonRenderer.toExportAst(exp, args.exportsOptions))}\n`;
126-
// }
127-
// }
128-
text = `${text}${text && exports ? '\n' : ''}${exports}`;
159+
text = `${text}${text && nodes ? '\n\n' : ''}${nodes}`;
129160

130161
if (args.trailingNewline === false && text.endsWith('\n')) {
131162
text = text.slice(0, -1);
@@ -134,27 +165,15 @@ export class PythonRenderer implements Renderer {
134165
return text;
135166
}
136167

137-
// static toExportAst(group: ModuleExport, options?: ExportsOptions): ts.ExportDeclaration {
138-
// const specifiers = group.exports.map((exp) => {
139-
// const specifier = ts.factory.createExportSpecifier(
140-
// exp.isTypeOnly,
141-
// exp.sourceName !== exp.exportedName ? $.id(exp.sourceName).toAst() : undefined,
142-
// $.id(exp.exportedName).toAst(),
143-
// );
144-
// return specifier;
145-
// });
146-
// const exportClause = group.namespaceExport
147-
// ? ts.factory.createNamespaceExport($.id(group.namespaceExport).toAst())
148-
// : (!group.canExportAll || !options?.preferExportAll) && specifiers.length
149-
// ? ts.factory.createNamedExports(specifiers)
150-
// : undefined;
151-
// return ts.factory.createExportDeclaration(
152-
// undefined,
153-
// group.isTypeOnly,
154-
// exportClause,
155-
// $.literal(group.modulePath).toAst(),
156-
// );
157-
// }
168+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
169+
static toExportAst(group: ModuleExport, options?: ExportsOptions): py.Assignment {
170+
const specifiers = group.exports.map((exp) => py.factory.createLiteral(exp.exportedName));
171+
return py.factory.createAssignment(
172+
py.factory.createIdentifier('__all__'),
173+
undefined,
174+
py.factory.createListExpression(specifiers),
175+
);
176+
}
158177

159178
static toImportAst(group: ModuleImport): py.ImportStatement {
160179
const names: Array<{
@@ -167,71 +186,71 @@ export class PythonRenderer implements Renderer {
167186
return py.factory.createImportStatement(group.modulePath, names, group.imports.length > 0);
168187
}
169188

170-
// private getExports(ctx: RenderContext): Exports {
171-
// type ModuleEntry = {
172-
// group: ModuleExport;
173-
// sortKey: SortKey;
174-
// };
175-
176-
// const groups = new Map<SortGroup, Map<SortModule, ModuleEntry>>();
177-
178-
// for (const exp of ctx.file.exports) {
179-
// const sortKey = moduleSortKey({
180-
// file: ctx.file,
181-
// fromFile: exp.from,
182-
// preferFileExtension: this._preferFileExtension,
183-
// root: ctx.project.root,
184-
// });
185-
// const modulePath = this._resolveModuleName?.(sortKey[2]) ?? sortKey[2];
186-
// const [groupIndex] = sortKey;
187-
188-
// if (!groups.has(groupIndex)) groups.set(groupIndex, new Map());
189-
// const moduleMap = groups.get(groupIndex)!;
190-
191-
// if (!moduleMap.has(modulePath)) {
192-
// moduleMap.set(modulePath, {
193-
// group: {
194-
// canExportAll: exp.canExportAll,
195-
// exports: exp.exports,
196-
// isTypeOnly: exp.isTypeOnly,
197-
// modulePath,
198-
// namespaceExport: exp.namespaceExport,
199-
// },
200-
// sortKey,
201-
// });
202-
// }
203-
// }
204-
205-
// const exports: Array<Array<ModuleExport>> = Array.from(groups.entries())
206-
// .sort((a, b) => a[0] - b[0])
207-
// .map(([, moduleMap]) => {
208-
// const entries = Array.from(moduleMap.values());
209-
210-
// entries.sort((a, b) => {
211-
// const d = a.sortKey[1] - b.sortKey[1];
212-
// return d !== 0 ? d : a.group.modulePath.localeCompare(b.group.modulePath);
213-
// });
214-
215-
// return entries.map((e) => {
216-
// const group = e.group;
217-
// if (group.namespaceExport) {
218-
// group.exports = [];
219-
// } else {
220-
// const isTypeOnly = !group.exports.find((exp) => !exp.isTypeOnly);
221-
// if (isTypeOnly) {
222-
// group.isTypeOnly = true;
223-
// for (const exp of group.exports) {
224-
// exp.isTypeOnly = false;
225-
// }
226-
// }
227-
// group.exports.sort((a, b) => a.exportedName.localeCompare(b.exportedName));
228-
// }
229-
// return group;
230-
// });
231-
// });
232-
233-
// return exports;
234-
// }
189+
private getExports(ctx: RenderContext): Exports {
190+
type ModuleEntry = {
191+
group: ModuleExport;
192+
sortKey: SortKey;
193+
};
194+
195+
const groups = new Map<SortGroup, Map<SortModule, ModuleEntry>>();
196+
197+
for (const exp of ctx.file.exports) {
198+
const sortKey = moduleSortKey({
199+
file: ctx.file,
200+
fromFile: exp.from,
201+
preferFileExtension: this._preferFileExtension,
202+
root: ctx.project.root,
203+
});
204+
const modulePath = this._resolveModuleName?.(sortKey[2]) ?? sortKey[2];
205+
const [groupIndex] = sortKey;
206+
207+
if (!groups.has(groupIndex)) groups.set(groupIndex, new Map());
208+
const moduleMap = groups.get(groupIndex)!;
209+
210+
if (!moduleMap.has(modulePath)) {
211+
moduleMap.set(modulePath, {
212+
group: {
213+
canExportAll: exp.canExportAll,
214+
exports: exp.exports,
215+
isTypeOnly: exp.isTypeOnly,
216+
modulePath,
217+
namespaceExport: exp.namespaceExport,
218+
},
219+
sortKey,
220+
});
221+
}
222+
}
223+
224+
const exports: Array<Array<ModuleExport>> = Array.from(groups.entries())
225+
.sort((a, b) => a[0] - b[0])
226+
.map(([, moduleMap]) => {
227+
const entries = Array.from(moduleMap.values());
228+
229+
entries.sort((a, b) => {
230+
const d = a.sortKey[1] - b.sortKey[1];
231+
return d !== 0 ? d : a.group.modulePath.localeCompare(b.group.modulePath);
232+
});
233+
234+
return entries.map((e) => {
235+
const group = e.group;
236+
if (group.namespaceExport) {
237+
group.exports = [];
238+
} else {
239+
const isTypeOnly = !group.exports.find((exp) => !exp.isTypeOnly);
240+
if (isTypeOnly) {
241+
group.isTypeOnly = true;
242+
for (const exp of group.exports) {
243+
exp.isTypeOnly = false;
244+
}
245+
}
246+
group.exports.sort((a, b) => a.exportedName.localeCompare(b.exportedName));
247+
}
248+
return group;
249+
});
250+
});
251+
252+
return exports;
253+
}
235254

236255
private getImports(ctx: RenderContext): Imports {
237256
type ModuleEntry = {

0 commit comments

Comments
 (0)