Skip to content

Commit d9028c8

Browse files
authored
feat(jsii-rosetta transliterate): transliterate a jsii assembly (#2869)
The new `jsii-rosetta transliterate` command can be used to transliterate one or more jsii assemblies (the `.jsii` file) to one or more target languages. The output assembly has all code examples transliterated into the correct target language. The current feature is experimental. It does not include renaming API elements within the assembly (only code examples are touched) - this feature may be added in the future. --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
1 parent e538b36 commit d9028c8

19 files changed

+1280
-34
lines changed

packages/jsii-pacmak/lib/targets/dotnet/dotnetdocgenerator.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as spec from '@jsii/spec';
22
import { CodeMaker } from 'codemaker';
33
import {
44
Rosetta,
5+
TargetLanguage,
56
Translation,
67
enforcesStrictMode,
78
typeScriptSnippetFromSource,
@@ -166,7 +167,10 @@ export class DotNetDocGenerator {
166167
'example',
167168
enforcesStrictMode(this.assembly),
168169
);
169-
const translated = this.rosetta.translateSnippet(snippet, 'csharp');
170+
const translated = this.rosetta.translateSnippet(
171+
snippet,
172+
TargetLanguage.CSHARP,
173+
);
170174
if (!translated) {
171175
return example;
172176
}
@@ -176,7 +180,7 @@ export class DotNetDocGenerator {
176180
private convertSamplesInMarkdown(markdown: string): string {
177181
return this.rosetta.translateSnippetsInMarkdown(
178182
markdown,
179-
'csharp',
183+
TargetLanguage.CSHARP,
180184
enforcesStrictMode(this.assembly),
181185
(trans) => ({
182186
language: trans.language,

packages/jsii-pacmak/lib/targets/java.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as fs from 'fs-extra';
55
import * as reflect from 'jsii-reflect';
66
import {
77
Rosetta,
8+
TargetLanguage,
89
typeScriptSnippetFromSource,
910
Translation,
1011
enforcesStrictMode,
@@ -2916,7 +2917,10 @@ class JavaGenerator extends Generator {
29162917
'example',
29172918
enforcesStrictMode(this.assembly),
29182919
);
2919-
const translated = this.rosetta.translateSnippet(snippet, 'java');
2920+
const translated = this.rosetta.translateSnippet(
2921+
snippet,
2922+
TargetLanguage.JAVA,
2923+
);
29202924
if (!translated) {
29212925
return example;
29222926
}
@@ -2926,7 +2930,7 @@ class JavaGenerator extends Generator {
29262930
private convertSamplesInMarkdown(markdown: string): string {
29272931
return this.rosetta.translateSnippetsInMarkdown(
29282932
markdown,
2929-
'java',
2933+
TargetLanguage.JAVA,
29302934
enforcesStrictMode(this.assembly),
29312935
(trans) => ({
29322936
language: trans.language,

packages/jsii-pacmak/lib/targets/python.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as escapeStringRegexp from 'escape-string-regexp';
44
import * as fs from 'fs-extra';
55
import * as reflect from 'jsii-reflect';
66
import {
7+
TargetLanguage,
78
Translation,
89
Rosetta,
910
enforcesStrictMode,
@@ -2290,7 +2291,10 @@ class PythonGenerator extends Generator {
22902291
'example',
22912292
enforcesStrictMode(this.assembly),
22922293
);
2293-
const translated = this.rosetta.translateSnippet(snippet, 'python');
2294+
const translated = this.rosetta.translateSnippet(
2295+
snippet,
2296+
TargetLanguage.PYTHON,
2297+
);
22942298
if (!translated) {
22952299
return example;
22962300
}
@@ -2300,7 +2304,7 @@ class PythonGenerator extends Generator {
23002304
public convertMarkdown(markdown: string): string {
23012305
return this.rosetta.translateSnippetsInMarkdown(
23022306
markdown,
2303-
'python',
2307+
TargetLanguage.PYTHON,
23042308
enforcesStrictMode(this.assembly),
23052309
(trans) => ({
23062310
language: trans.language,

packages/jsii-rosetta/bin/jsii-rosetta.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
import { translateMarkdown } from '../lib/commands/convert';
1111
import { extractSnippets } from '../lib/commands/extract';
1212
import { readTablet } from '../lib/commands/read';
13+
import { transliterateAssembly } from '../lib/commands/transliterate';
14+
import { TargetLanguage } from '../lib/languages';
1315
import { PythonVisitor } from '../lib/languages/python';
1416
import { VisualizeAstVisitor } from '../lib/languages/visualize';
1517
import * as logging from '../lib/logging';
@@ -166,6 +168,60 @@ function main() {
166168
}
167169
}),
168170
)
171+
.command(
172+
'transliterate [ASSEMBLY..]',
173+
'(EXPERIMENTAL) Transliterates the designated assemblies',
174+
(command) =>
175+
command
176+
.positional('ASSEMBLY', {
177+
type: 'string',
178+
string: true,
179+
default: new Array<string>(),
180+
required: true,
181+
describe: 'Assembly to transliterate',
182+
})
183+
.option('language', {
184+
alias: 'l',
185+
type: 'string',
186+
string: true,
187+
default: new Array<string>(),
188+
describe: 'Language ID to transliterate to',
189+
})
190+
.options('strict', {
191+
alias: 's',
192+
type: 'boolean',
193+
describe:
194+
'Fail if an example that needs live transliteration fails to compile (which could cause incorrect transpilation results)',
195+
})
196+
.option('tablet', {
197+
alias: 't',
198+
type: 'string',
199+
describe:
200+
'Language tablet containing pre-translated code examples to use (these are generated by the `extract` command)',
201+
}),
202+
wrapHandler((args) => {
203+
const assemblies = (
204+
args.ASSEMBLY.length > 0 ? args.ASSEMBLY : ['.']
205+
).map((dir) => path.resolve(process.cwd(), dir));
206+
const languages =
207+
args.language.length > 0
208+
? args.language.map((lang) => {
209+
const target = Object.entries(TargetLanguage).find(
210+
([k]) => k === lang,
211+
)?.[1];
212+
if (target == null) {
213+
throw new Error(
214+
`Unknown target language: ${lang}. Expected one of ${Object.keys(
215+
TargetLanguage,
216+
).join(', ')}`,
217+
);
218+
}
219+
return target;
220+
})
221+
: Object.values(TargetLanguage);
222+
return transliterateAssembly(assemblies, languages, args);
223+
}),
224+
)
169225
.command(
170226
'read <TABLET> [KEY] [LANGUAGE]',
171227
'Display snippets in a language tablet file',

packages/jsii-rosetta/jest.config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1-
import config from '../../jest.config';
1+
import { join } from 'path';
22

3-
export default config;
3+
import { overriddenConfig } from '../../jest.config';
4+
5+
export default overriddenConfig({
6+
setupFiles: [join(__dirname, 'jestsetup.js')],
7+
});

packages/jsii-rosetta/jestsetup.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Require `mock-fs` before `jest` initializes, as `mock-fs` relies on
2+
// hijacking the `fs` module, which `jest` also hijacks (and that needs to
3+
// happen last).
4+
require('mock-fs');
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { Assembly, Docs, SPEC_FILE_NAME, Type, TypeKind } from '@jsii/spec';
2+
import { readJson, writeJson } from 'fs-extra';
3+
import { resolve } from 'path';
4+
5+
import { fixturize } from '../fixtures';
6+
import { TargetLanguage } from '../languages';
7+
import { debug } from '../logging';
8+
import { Rosetta } from '../rosetta';
9+
import { SnippetParameters, typeScriptSnippetFromSource } from '../snippet';
10+
import { Translation } from '../tablets/tablets';
11+
12+
export interface TransliterateAssemblyOptions {
13+
/**
14+
* Whather transliteration should fail upon failing to compile an example that
15+
* required live transliteration.
16+
*
17+
* @default false
18+
*/
19+
readonly strict?: boolean;
20+
21+
/**
22+
* A pre-build translation tablet (as produced by `jsii-rosetta extract`).
23+
*
24+
* @default - Only the default tablet (`.jsii.tabl.json`) files will be used.
25+
*/
26+
readonly tablet?: string;
27+
}
28+
29+
/**
30+
* Prepares transliterated versions of the designated assemblies into the
31+
* selected taregt languages.
32+
*
33+
* @param assemblyLocations the directories which contain assemblies to
34+
* transliterate.
35+
* @param targetLanguages the languages into which to transliterate.
36+
* @param tabletLocation an optional Rosetta tablet file to source
37+
* pre-transliterated snippets from.
38+
*
39+
* @experimental
40+
*/
41+
export async function transliterateAssembly(
42+
assemblyLocations: readonly string[],
43+
targetLanguages: readonly TargetLanguage[],
44+
options: TransliterateAssemblyOptions = {},
45+
): Promise<void> {
46+
const rosetta = new Rosetta({
47+
includeCompilerDiagnostics: true,
48+
liveConversion: true,
49+
targetLanguages,
50+
});
51+
if (options.tablet) {
52+
await rosetta.loadTabletFromFile(options.tablet);
53+
}
54+
const assemblies = await loadAssemblies(assemblyLocations, rosetta);
55+
56+
for (const [location, loadAssembly] of assemblies.entries()) {
57+
for (const language of targetLanguages) {
58+
const now = new Date().getTime();
59+
// eslint-disable-next-line no-await-in-loop
60+
const result = await loadAssembly();
61+
if (result.readme?.markdown) {
62+
result.readme.markdown = rosetta.translateSnippetsInMarkdown(
63+
result.readme.markdown,
64+
language,
65+
true /* strict */,
66+
(translation) => ({
67+
language: translation.language,
68+
source: prefixDisclaimer(translation),
69+
}),
70+
location,
71+
);
72+
}
73+
for (const type of Object.values(result.types ?? {})) {
74+
transliterateType(type, rosetta, language, location);
75+
}
76+
// eslint-disable-next-line no-await-in-loop
77+
await writeJson(
78+
resolve(location, `${SPEC_FILE_NAME}.${language}`),
79+
result,
80+
{ spaces: 2 },
81+
);
82+
const then = new Date().getTime();
83+
debug(
84+
`Done transliterating ${result.name}@${
85+
result.version
86+
} to ${language} after ${then - now} milliseconds`,
87+
);
88+
}
89+
}
90+
91+
rosetta.printDiagnostics(process.stderr);
92+
if (rosetta.hasErrors && options.strict) {
93+
throw new Error(
94+
'Strict mode is enabled and some examples failed compilation!',
95+
);
96+
}
97+
}
98+
99+
/**
100+
* Given a set of directories containing `.jsii` assemblies, load all the
101+
* assemblies into the provided `Rosetta` instance and return a map of
102+
* directories to assembly-loading functions (the function re-loads the original
103+
* assembly from disk on each invocation).
104+
*
105+
* @param directories the assembly-containing directories to traverse.
106+
* @param rosetta the `Rosetta` instance in which to load assemblies.
107+
*
108+
* @returns a map of directories to a function that loads the `.jsii` assembly
109+
* contained therein from disk.
110+
*/
111+
async function loadAssemblies(
112+
directories: readonly string[],
113+
rosetta: Rosetta,
114+
): Promise<ReadonlyMap<string, AssemblyLoader>> {
115+
const result = new Map<string, AssemblyLoader>();
116+
117+
for (const directory of directories) {
118+
const loader = () => readJson(resolve(directory, SPEC_FILE_NAME));
119+
// eslint-disable-next-line no-await-in-loop
120+
await rosetta.addAssembly(await loader(), directory);
121+
result.set(directory, loader);
122+
}
123+
124+
return result;
125+
}
126+
127+
type Mutable<T> = { -readonly [K in keyof T]: Mutable<T[K]> };
128+
type AssemblyLoader = () => Promise<Mutable<Assembly>>;
129+
130+
function prefixDisclaimer(translation: Translation): string {
131+
const message = translation.didCompile
132+
? 'Example automatically generated. See https://github.com/aws/jsii/issues/826'
133+
: 'Example automatically generated without compilation. See https://github.com/aws/jsii/issues/826';
134+
return `${commentToken()} ${message}\n${translation.source}`;
135+
136+
function commentToken() {
137+
// This is future-proofed a bit, but don't read too much in this...
138+
switch (translation.language) {
139+
case 'python':
140+
case 'ruby':
141+
return '#';
142+
case 'csharp':
143+
case 'java':
144+
case 'go':
145+
default:
146+
return '//';
147+
}
148+
}
149+
}
150+
151+
function transliterateType(
152+
type: Type,
153+
rosetta: Rosetta,
154+
language: TargetLanguage,
155+
workingDirectory: string,
156+
): void {
157+
transliterateDocs(type.docs);
158+
switch (type.kind) {
159+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
160+
// @ts-ignore 7029
161+
case TypeKind.Class:
162+
transliterateDocs(type?.initializer?.docs);
163+
164+
// fallthrough
165+
case TypeKind.Interface:
166+
for (const method of type.methods ?? []) {
167+
transliterateDocs(method.docs);
168+
for (const parameter of method.parameters ?? []) {
169+
transliterateDocs(parameter.docs);
170+
}
171+
}
172+
for (const property of type.properties ?? []) {
173+
transliterateDocs(property.docs);
174+
}
175+
break;
176+
177+
case TypeKind.Enum:
178+
for (const member of type.members) {
179+
transliterateDocs(member.docs);
180+
}
181+
break;
182+
183+
default:
184+
throw new Error(`Unsupported type kind: ${(type as any).kind}`);
185+
}
186+
187+
function transliterateDocs(docs: Docs | undefined) {
188+
if (docs?.example) {
189+
const snippet = fixturize(
190+
typeScriptSnippetFromSource(
191+
docs.example,
192+
'example',
193+
true /* strict */,
194+
{ [SnippetParameters.$PROJECT_DIRECTORY]: workingDirectory },
195+
),
196+
);
197+
const translation = rosetta.translateSnippet(snippet, language);
198+
if (translation != null) {
199+
docs.example = prefixDisclaimer(translation);
200+
}
201+
}
202+
}
203+
}

packages/jsii-rosetta/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './translate';
22
export { renderTree } from './o-tree';
3+
export { TargetLanguage } from './languages/target-language';
34
export { CSharpVisitor } from './languages/csharp';
45
export { JavaVisitor } from './languages/java';
56
export { PythonVisitor } from './languages/python';

packages/jsii-rosetta/lib/languages/csharp.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from '../typescript/types';
2424
import { flat, partition, setExtend } from '../util';
2525
import { DefaultVisitor } from './default';
26+
import { TargetLanguage } from './target-language';
2627

2728
interface CSharpLanguageContext {
2829
/**
@@ -74,7 +75,7 @@ interface CSharpLanguageContext {
7475
type CSharpRenderer = AstRenderer<CSharpLanguageContext>;
7576

7677
export class CSharpVisitor extends DefaultVisitor<CSharpLanguageContext> {
77-
public readonly language = 'csharp';
78+
public readonly language = TargetLanguage.CSHARP;
7879

7980
public readonly defaultContext = {
8081
propertyOrMethod: false,

0 commit comments

Comments
 (0)