Skip to content

Commit ec989ea

Browse files
committed
minor(emitter-typescript): update types to use proper namespacing
1 parent d503233 commit ec989ea

File tree

3 files changed

+130
-120
lines changed

3 files changed

+130
-120
lines changed

.changeset/famous-gorillas-fix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@typespec-tools/emitter-typescript": minor
3+
---
4+
5+
Update types to use proper namespacing

packages/emitter-typescript/src/emitter.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
IntrinsicType,
1010
Model,
1111
ModelProperty,
12+
Namespace,
1213
NumericLiteral,
1314
Operation,
1415
Scalar,
@@ -31,6 +32,7 @@ import {
3132
SourceFile,
3233
SourceFileScope,
3334
StringBuilder,
35+
TypeSpecDeclaration,
3436
} from "@typespec/compiler/emitter-framework";
3537
import { EmitterOptions } from "./lib.js";
3638

@@ -52,9 +54,109 @@ export const intrinsicNameToTSType = new Map<string, string>([
5254
["void", "void"],
5355
]);
5456

57+
function emitNamespaces(scope: Scope<string>) {
58+
let res = "";
59+
for (const childScope of scope.childScopes) {
60+
res += emitNamespace(childScope);
61+
}
62+
return res;
63+
}
64+
function emitNamespace(scope: Scope<string>) {
65+
let ns = `namespace ${scope.name} {\n`;
66+
ns += emitNamespaces(scope);
67+
for (const decl of scope.declarations) {
68+
ns += decl.value + "\n";
69+
}
70+
ns += `}\n`;
71+
72+
return ns;
73+
}
74+
75+
function getNamespaceChain(decl: { namespace?: Namespace }): string[] {
76+
let ns = [decl.namespace?.name];
77+
let parent = decl.namespace?.namespace;
78+
while (parent) {
79+
ns.push(parent.name);
80+
parent = parent.namespace;
81+
}
82+
return ns.filter((n): n is string => !!n).reverse();
83+
}
84+
5585
export class TypescriptEmitter<
5686
TEmitterOptions extends object = EmitterOptions,
5787
> extends CodeTypeEmitter<TEmitterOptions> {
88+
private nsByName: Map<string, Scope<string>> = new Map();
89+
90+
declarationContext(
91+
decl: TypeSpecDeclaration & { namespace?: Namespace }
92+
): Context {
93+
const name = decl.namespace?.name;
94+
if (!name) return {};
95+
96+
const namespaceChain = getNamespaceChain(decl);
97+
98+
let nsScope = this.nsByName.get(name);
99+
if (!nsScope) {
100+
// If there is no scope for the namespace, create one for each
101+
// namespace in the chain.
102+
let parentScope: Scope<string> | undefined;
103+
while (namespaceChain.length > 0) {
104+
const ns = namespaceChain.shift();
105+
if (!ns) {
106+
break;
107+
}
108+
nsScope = this.nsByName.get(ns);
109+
if (nsScope) {
110+
parentScope = nsScope;
111+
continue;
112+
}
113+
nsScope = this.emitter.createScope(
114+
{},
115+
ns,
116+
parentScope ?? this.emitter.getContext().scope
117+
);
118+
this.nsByName.set(ns, nsScope);
119+
parentScope = nsScope;
120+
}
121+
}
122+
123+
return {
124+
scope: nsScope,
125+
};
126+
}
127+
128+
modelDeclarationContext(model: Model): Context {
129+
return this.declarationContext(model);
130+
}
131+
132+
modelInstantiationContext(model: Model): Context {
133+
return this.declarationContext(model);
134+
}
135+
136+
unionDeclarationContext(union: Union): Context {
137+
return this.declarationContext(union);
138+
}
139+
140+
unionInstantiationContext(union: Union): Context {
141+
return this.declarationContext(union);
142+
}
143+
144+
enumDeclarationContext(en: Enum): Context {
145+
return this.declarationContext(en);
146+
}
147+
148+
arrayDeclarationContext(array: Model): Context {
149+
return this.declarationContext(array);
150+
}
151+
152+
interfaceDeclarationContext(iface: Interface): Context {
153+
return this.declarationContext(iface);
154+
}
155+
156+
operationDeclarationContext(operation: Operation): Context {
157+
return this.declarationContext(operation);
158+
}
159+
58160
// type literals
59161
booleanLiteral(boolean: BooleanLiteral): EmitterOutput<string> {
60162
return JSON.stringify(boolean.value);
@@ -312,6 +414,8 @@ export class TypescriptEmitter<
312414
emittedSourceFile.contents += decl.value + "\n";
313415
}
314416

417+
emittedSourceFile.contents += emitNamespaces(sourceFile.globalScope);
418+
315419
emittedSourceFile.contents = await prettier.format(
316420
emittedSourceFile.contents,
317421
{

packages/emitter-typescript/test/emitter.test.ts

Lines changed: 21 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
1-
import { Enum, Interface, Model, Operation, Union } from "@typespec/compiler";
1+
import { Model } from "@typespec/compiler";
22
import {
33
AssetEmitter,
44
Context,
5-
EmittedSourceFile,
65
EmitterOutput,
7-
Scope,
8-
SourceFile,
96
TypeSpecDeclaration,
107
createAssetEmitter,
118
} from "@typespec/compiler/emitter-framework";
129

1310
import assert from "assert";
14-
import * as prettier from "prettier";
1511
import { describe, it } from "vitest";
1612

1713
import {
@@ -22,28 +18,36 @@ import { EmitterOptions } from "../src/lib.js";
2218
import { emitTypeSpec, getHostForTypeSpecFile } from "./host.js";
2319

2420
const testCode = `
21+
namespace Root;
22+
2523
model Basic { x: string }
2624
model RefsOtherModel { x: Basic, y: UnionDecl }
2725
model HasNestedLiteral { x: { y: string } }
2826
model HasArrayProperty { x: string[], y: Basic[] }
2927
model IsArray is Array<string>;
30-
model Derived extends Basic { }
3128
32-
@doc("Has a doc")
33-
model HasDoc { @doc("an x property") x: string }
29+
namespace WrappedModels {
30+
model Derived extends Basic { }
31+
32+
@doc("Has a doc")
33+
model HasDoc { @doc("an x property") x: string }
34+
}
3435
3536
model Template<T> { prop: T }
3637
model HasTemplates { x: Template<Basic> }
3738
model IsTemplate is Template<Basic>;
3839
model HasRef {
3940
x: Basic.x;
4041
y: RefsOtherModel.x;
42+
z: Operations.SomeOp;
4143
}
4244
43-
op SomeOp(x: string): string;
45+
namespace Operations {
46+
op SomeOp(x: string): string;
4447
45-
interface MyInterface {
46-
op get(): string;
48+
interface MyInterface {
49+
op get(): string;
50+
}
4751
}
4852
4953
union UnionDecl {
@@ -58,11 +62,6 @@ enum MyEnum {
5862
`;
5963

6064
class SingleFileTestEmitter extends SingleFileTypescriptEmitter {
61-
programContext(): Context {
62-
const outputFile = this.emitter.createSourceFile("output.ts");
63-
return { scope: outputFile.globalScope };
64-
}
65-
6665
operationReturnTypeReferenceContext(): Context {
6766
return {
6867
fromOperation: true,
@@ -351,39 +350,7 @@ describe("emitter-framework: typescript emitter", () => {
351350
const host = await getHostForTypeSpecFile(testCode);
352351

353352
class ClassPerFileEmitter extends TypescriptEmitter {
354-
modelDeclarationContext(model: Model): Context {
355-
return this.#declarationContext(model);
356-
}
357-
358-
modelInstantiationContext(model: Model): Context {
359-
return this.#declarationContext(model);
360-
}
361-
362-
unionDeclarationContext(union: Union): Context {
363-
return this.#declarationContext(union);
364-
}
365-
366-
unionInstantiationContext(union: Union): Context {
367-
return this.#declarationContext(union);
368-
}
369-
370-
enumDeclarationContext(en: Enum): Context {
371-
return this.#declarationContext(en);
372-
}
373-
374-
arrayDeclarationContext(array: Model): Context {
375-
return this.#declarationContext(array);
376-
}
377-
378-
interfaceDeclarationContext(iface: Interface): Context {
379-
return this.#declarationContext(iface);
380-
}
381-
382-
operationDeclarationContext(operation: Operation): Context {
383-
return this.#declarationContext(operation);
384-
}
385-
386-
#declarationContext(decl: TypeSpecDeclaration) {
353+
declarationContext(decl: TypeSpecDeclaration) {
387354
const name = this.emitter.emitDeclarationName(decl);
388355
const outputFile = this.emitter.createSourceFile(`${name}.ts`);
389356

@@ -422,77 +389,11 @@ describe("emitter-framework: typescript emitter", () => {
422389
});
423390

424391
it("emits to namespaces", async () => {
425-
const host = await getHostForTypeSpecFile(testCode);
426-
427-
class NamespacedEmitter extends SingleFileTypescriptEmitter {
428-
private nsByName: Map<string, Scope<string>> = new Map();
429-
430-
modelDeclarationContext(model: Model): Context {
431-
const name = this.emitter.emitDeclarationName(model);
432-
if (!name) return {};
433-
const nsName = name.slice(0, 1);
434-
let nsScope = this.nsByName.get(nsName);
435-
if (!nsScope) {
436-
nsScope = this.emitter.createScope(
437-
{},
438-
nsName,
439-
this.emitter.getContext().scope
440-
);
441-
this.nsByName.set(nsName, nsScope);
442-
}
443-
444-
return {
445-
scope: nsScope,
446-
};
447-
}
448-
449-
async sourceFile(
450-
sourceFile: SourceFile<string>
451-
): Promise<EmittedSourceFile> {
452-
const emittedSourceFile = await super.sourceFile(sourceFile);
453-
emittedSourceFile.contents += emitNamespaces(sourceFile.globalScope);
454-
emittedSourceFile.contents = await prettier.format(
455-
emittedSourceFile.contents,
456-
{
457-
parser: "typescript",
458-
}
459-
);
460-
return emittedSourceFile;
461-
462-
function emitNamespaces(scope: Scope<string>) {
463-
let res = "";
464-
for (const childScope of scope.childScopes) {
465-
res += emitNamespace(childScope);
466-
}
467-
return res;
468-
}
469-
function emitNamespace(scope: Scope<string>) {
470-
let ns = `namespace ${scope.name} {\n`;
471-
ns += emitNamespaces(scope);
472-
for (const decl of scope.declarations) {
473-
ns += decl.value + "\n";
474-
}
475-
ns += `}\n`;
476-
477-
return ns;
478-
}
479-
}
480-
}
481-
const emitter = createAssetEmitter(host.program, NamespacedEmitter, {
482-
emitterOutputDir: host.program.compilerOptions.outputDir!,
483-
options: {},
484-
} as any);
485-
emitter.emitProgram();
486-
await emitter.writeOutput();
487-
const contents = (await host.compilerHost.readFile("tsp-output/output.ts"))
488-
.text;
489-
assert.match(contents, /namespace B/);
490-
assert.match(contents, /namespace R/);
491-
assert.match(contents, /namespace H/);
492-
assert.match(contents, /namespace I/);
493-
assert.match(contents, /namespace D/);
494-
assert.match(contents, /B\.Basic/);
495-
assert.match(contents, /B\.Basic/);
392+
const contents = await emitTypeSpecToTs(testCode);
393+
assert.match(contents, /namespace Root/);
394+
assert.match(contents, /namespace Operations/);
395+
assert.match(contents, /namespace WrappedModels/);
396+
assert.match(contents, /Operations\.SomeOp/);
496397
});
497398

498399
it("handles circular references", async () => {

0 commit comments

Comments
 (0)