diff --git a/packages/jsii-pacmak/lib/targets/go.ts b/packages/jsii-pacmak/lib/targets/go.ts index 01a7422f54..4b69472b52 100644 --- a/packages/jsii-pacmak/lib/targets/go.ts +++ b/packages/jsii-pacmak/lib/targets/go.ts @@ -94,10 +94,11 @@ export class Golang extends Target { // local build directory for this module. if // we do, add a "replace" directive to point to it instead of download from // the network. - const visit = (pkg: RootPackage) => { + const visit = async (pkg: RootPackage) => { for (const dep of pkg.packageDependencies) { for (const baseDir of dirs) { - const moduleDir = tryFindLocalModule(baseDir, dep); + // eslint-disable-next-line no-await-in-loop + const moduleDir = await tryFindLocalModule(baseDir, dep); if (moduleDir) { replace[dep.goModuleName] = moduleDir; @@ -107,11 +108,12 @@ export class Golang extends Target { } // recurse to transitive deps ("replace" is only considered at the top level go.mod) - visit(dep); + // eslint-disable-next-line no-await-in-loop + await visit(dep); } }; - visit(this.goGenerator.rootPackage); + await visit(this.goGenerator.rootPackage); // write `local.go.mod` @@ -198,14 +200,14 @@ class GoGenerator implements IGenerator { * @param baseDir the `dist/go` directory * @returns `undefined` if not or the module directory otherwise. */ -function tryFindLocalModule(baseDir: string, pkg: RootPackage) { +async function tryFindLocalModule(baseDir: string, pkg: RootPackage) { const gomodPath = path.join(baseDir, pkg.packageName, GOMOD_FILENAME); - if (!fs.pathExistsSync(gomodPath)) { + if (!(await fs.pathExists(gomodPath))) { return undefined; } // read `go.mod` and check that it is for the correct module - const gomod = fs.readFileSync(gomodPath, 'utf-8').split('\n'); + const gomod = (await fs.readFile(gomodPath, 'utf-8')).split('\n'); const isExpectedModule = gomod.find( (line) => line.trim() === `module ${pkg.goModuleName}`, ); diff --git a/packages/jsii-rosetta/bin/jsii-rosetta.ts b/packages/jsii-rosetta/bin/jsii-rosetta.ts index da6eba97a5..4e0711074f 100644 --- a/packages/jsii-rosetta/bin/jsii-rosetta.ts +++ b/packages/jsii-rosetta/bin/jsii-rosetta.ts @@ -189,9 +189,17 @@ function main() { }) .options('strict', { alias: 's', - type: 'boolean', + conflicts: 'loose', describe: 'Fail if an example that needs live transliteration fails to compile (which could cause incorrect transpilation results)', + type: 'boolean', + }) + .options('loose', { + alias: 'l', + conflicts: 'strict', + describe: + 'Ignore missing fixtures and literate markdown files instead of failing', + type: 'boolean', }) .option('tablet', { alias: 't', diff --git a/packages/jsii-rosetta/lib/commands/extract.ts b/packages/jsii-rosetta/lib/commands/extract.ts index 3ee8f971d2..f3637f7651 100644 --- a/packages/jsii-rosetta/lib/commands/extract.ts +++ b/packages/jsii-rosetta/lib/commands/extract.ts @@ -28,6 +28,7 @@ export interface ExtractOptions { export async function extractSnippets( assemblyLocations: string[], options: ExtractOptions, + loose = false, ): Promise { const only = options.only ?? []; @@ -37,7 +38,7 @@ export async function extractSnippets( options.validateAssemblies, ); - let snippets = allTypeScriptSnippets(assemblies); + let snippets = allTypeScriptSnippets(assemblies, loose); if (only.length > 0) { snippets = filterSnippets(snippets, only); } diff --git a/packages/jsii-rosetta/lib/commands/transliterate.ts b/packages/jsii-rosetta/lib/commands/transliterate.ts index 6b342609e1..2a50f1007f 100644 --- a/packages/jsii-rosetta/lib/commands/transliterate.ts +++ b/packages/jsii-rosetta/lib/commands/transliterate.ts @@ -11,7 +11,15 @@ import { Translation } from '../tablets/tablets'; export interface TransliterateAssemblyOptions { /** - * Whather transliteration should fail upon failing to compile an example that + * Whether to ignore any missing fixture files or literate markdown documents + * referenced by the assembly, instead of failing. + * + * @default false + */ + readonly loose?: boolean; + + /** + * Whether transliteration should fail upon failing to compile an example that * required live transliteration. * * @default false @@ -46,6 +54,7 @@ export async function transliterateAssembly( const rosetta = new Rosetta({ includeCompilerDiagnostics: true, liveConversion: true, + loose: options.loose, targetLanguages, }); if (options.tablet) { @@ -71,7 +80,7 @@ export async function transliterateAssembly( ); } for (const type of Object.values(result.types ?? {})) { - transliterateType(type, rosetta, language, location); + transliterateType(type, rosetta, language, location, options.loose); } // eslint-disable-next-line no-await-in-loop await writeJson( @@ -153,6 +162,7 @@ function transliterateType( rosetta: Rosetta, language: TargetLanguage, workingDirectory: string, + loose = false, ): void { transliterateDocs(type.docs); switch (type.kind) { @@ -193,6 +203,7 @@ function transliterateType( true /* strict */, { [SnippetParameters.$PROJECT_DIRECTORY]: workingDirectory }, ), + loose, ); const translation = rosetta.translateSnippet(snippet, language); if (translation != null) { diff --git a/packages/jsii-rosetta/lib/fixtures.ts b/packages/jsii-rosetta/lib/fixtures.ts index 81f3d09979..09fc28ad07 100644 --- a/packages/jsii-rosetta/lib/fixtures.ts +++ b/packages/jsii-rosetta/lib/fixtures.ts @@ -12,7 +12,10 @@ import { TypeScriptSnippet, SnippetParameters } from './snippet'; /** * Complete snippets with fixtures, if required */ -export function fixturize(snippet: TypeScriptSnippet): TypeScriptSnippet { +export function fixturize( + snippet: TypeScriptSnippet, + loose = false, +): TypeScriptSnippet { let source = snippet.visibleSource; const parameters = snippet.parameters ?? {}; @@ -25,14 +28,14 @@ export function fixturize(snippet: TypeScriptSnippet): TypeScriptSnippet { if (literateSource) { // Compatibility with the "old school" example inclusion mechanism. // Completely load this file and attach a parameter with its directory. - source = loadLiterateSource(directory, literateSource); + source = loadLiterateSource(directory, literateSource, loose); parameters[SnippetParameters.$COMPILATION_DIRECTORY] = path.join( directory, path.dirname(literateSource), ); } else if (parameters[SnippetParameters.FIXTURE]) { - // Explicitly request a fixture - source = loadAndSubFixture(directory, parameters.fixture, source, true); + // Explicitly requested fixture must exist, unless we are operating in loose mode + source = loadAndSubFixture(directory, parameters.fixture, source, !loose); } else if (parameters[SnippetParameters.NO_FIXTURE] === undefined) { // Don't explicitly request no fixture source = loadAndSubFixture(directory, 'default', source, false); @@ -45,10 +48,22 @@ export function fixturize(snippet: TypeScriptSnippet): TypeScriptSnippet { }; } -function loadLiterateSource(directory: string, literateFileName: string) { +function loadLiterateSource( + directory: string, + literateFileName: string, + loose = false, +) { const fullPath = path.join(directory, literateFileName); const exists = fs.existsSync(fullPath); if (!exists) { + if (loose) { + // In loose mode, we'll fall back to the `.js` file if it exists... + const jsFile = fullPath.replace(/\.ts(x?)$/, '.js$1'); + if (fs.existsSync(jsFile)) { + return fs.readFileSync(jsFile, { encoding: 'utf-8' }); + } + return `Missing literate source file ${literateFileName}`; + } // This couldn't really happen in practice, but do the check anyway throw new Error( `Sample uses literate source ${literateFileName}, but not found: ${fullPath}`, diff --git a/packages/jsii-rosetta/lib/jsii/assemblies.ts b/packages/jsii-rosetta/lib/jsii/assemblies.ts index 0f3ab7545a..0b87fea897 100644 --- a/packages/jsii-rosetta/lib/jsii/assemblies.ts +++ b/packages/jsii-rosetta/lib/jsii/assemblies.ts @@ -134,6 +134,7 @@ function removeSlashes(x: string) { export function* allTypeScriptSnippets( assemblies: readonly LoadedAssembly[], + loose = false, ): IterableIterator { for (const { assembly, directory } of assemblies) { const strict = enforcesStrictMode(assembly); @@ -146,7 +147,7 @@ export function* allTypeScriptSnippets( [SnippetParameters.$PROJECT_DIRECTORY]: directory, }, ); - yield fixturize(snippet); + yield fixturize(snippet, loose); break; case 'markdown': for (const snippet of extractTypescriptSnippetsFromMarkdown( @@ -157,7 +158,7 @@ export function* allTypeScriptSnippets( const withDirectory = updateParameters(snippet, { [SnippetParameters.$PROJECT_DIRECTORY]: directory, }); - yield fixturize(withDirectory); + yield fixturize(withDirectory, loose); } } } diff --git a/packages/jsii-rosetta/lib/rosetta.ts b/packages/jsii-rosetta/lib/rosetta.ts index 838a331ec7..afb126107d 100644 --- a/packages/jsii-rosetta/lib/rosetta.ts +++ b/packages/jsii-rosetta/lib/rosetta.ts @@ -41,6 +41,14 @@ export interface RosettaOptions { * Whether to include compiler diagnostics in the compilation results. */ readonly includeCompilerDiagnostics?: boolean; + + /** + * Whether this Rosetta should operate in "loose" mode, where missing literate + * source files and missing fixtures are ignored instead of failing. + * + * @default false + */ + readonly loose?: boolean; } /** @@ -60,8 +68,10 @@ export class Rosetta { private readonly liveTablet = new LanguageTablet(); private readonly extractedSnippets = new Map(); private readonly translator: Translator; + private readonly loose: boolean; public constructor(private readonly options: RosettaOptions = {}) { + this.loose = !!options.loose; this.translator = new Translator( options.includeCompilerDiagnostics ?? false, ); @@ -114,9 +124,10 @@ export class Rosetta { } if (this.options.liveConversion) { - for (const tsnip of allTypeScriptSnippets([ - { assembly, directory: assemblyDir }, - ])) { + for (const tsnip of allTypeScriptSnippets( + [{ assembly, directory: assemblyDir }], + this.loose, + )) { this.extractedSnippets.set(tsnip.visibleSource, tsnip); } } diff --git a/packages/jsii-rosetta/test/commands/transliterate.test.ts b/packages/jsii-rosetta/test/commands/transliterate.test.ts index ecb85ef88e..6f11824fbb 100644 --- a/packages/jsii-rosetta/test/commands/transliterate.test.ts +++ b/packages/jsii-rosetta/test/commands/transliterate.test.ts @@ -907,6 +907,330 @@ export class ClassName implements IInterface { ); })); +test('single assembly, loose mode', () => + withTemporaryDirectory(async (tmpDir) => { + // GIVEN + const compilationResult = await jsii.compileJsiiForTest({ + 'README.md': ` +# Missing literate source + +[example is not found](missing-example.lit.ts) + +# Missing fixture + +\`\`\`ts fixture=not-found +new SampleClass('README.md'); +\`\`\` +`, + 'index.ts': ` +/** + * @example + * /// fixture=not-found + * new DoesNotCompile(this, 'That', { foo: 1337 }); + */ +export class SampleClass { + public constructor(_source: string){ } +} +`, + // The `lit.ts` source file will not be there in packaged form... + 'missing-example.lit.ts': ` +import { SampleClass } from './index'; + +/// !show +/// ## This is a heading within the literate file! +new SampleClass('literate'); + `, + }); + fs.writeJsonSync( + path.join(tmpDir, SPEC_FILE_NAME), + compilationResult.assembly, + { + spaces: 2, + }, + ); + for (const [file, content] of Object.entries(compilationResult.files)) { + fs.writeFileSync(path.resolve(tmpDir, file), content, 'utf-8'); + } + + // WHEN + await expect( + transliterateAssembly([tmpDir], Object.values(TargetLanguage), { + loose: true, + }), + ).resolves.not.toThrow(); + + // THEN + expect( + fs.readJsonSync(path.join(tmpDir, `${SPEC_FILE_NAME}.csharp`)), + ).toMatchInlineSnapshot( + { + fingerprint: expect.any(String), + jsiiVersion: expect.any(String), + }, + ` + Object { + "author": Object { + "name": "John Doe", + "roles": Array [ + "author", + ], + }, + "description": "testpkg", + "fingerprint": Any, + "homepage": "https://github.com/aws/jsii.git", + "jsiiVersion": Any, + "license": "Apache-2.0", + "metadata": Object { + "jsii": Object { + "pacmak": Object { + "hasDefaultInterfaces": true, + }, + }, + }, + "name": "testpkg", + "readme": Object { + "markdown": "# Missing literate source + + ## This is a heading within the literate file! + + \`\`\`csharp + // Example automatically generated without compilation. See https://github.com/aws/jsii/issues/826 + new index_1.SampleClass(\\"literate\\"); + \`\`\` + + # Missing fixture + + \`\`\`csharp + // Example automatically generated without compilation. See https://github.com/aws/jsii/issues/826 + new SampleClass(\\"README.md\\"); + \`\`\`", + }, + "repository": Object { + "type": "git", + "url": "https://github.com/aws/jsii.git", + }, + "schema": "jsii/0.10.0", + "targets": Object { + "js": Object { + "npm": "testpkg", + }, + }, + "types": Object { + "testpkg.SampleClass": Object { + "assembly": "testpkg", + "docs": Object { + "example": "// Example automatically generated without compilation. See https://github.com/aws/jsii/issues/826 + new DoesNotCompile(this, \\"That\\", new Struct { Foo = 1337 });", + }, + "fqn": "testpkg.SampleClass", + "initializer": Object { + "locationInModule": Object { + "filename": "index.ts", + "line": 8, + }, + "parameters": Array [ + Object { + "name": "_source", + "type": Object { + "primitive": "string", + }, + }, + ], + }, + "kind": "class", + "locationInModule": Object { + "filename": "index.ts", + "line": 7, + }, + "name": "SampleClass", + }, + }, + "version": "0.0.1", + } + `, + ); + + expect( + fs.readJsonSync(path.join(tmpDir, `${SPEC_FILE_NAME}.java`)), + ).toMatchInlineSnapshot( + { + fingerprint: expect.any(String), + jsiiVersion: expect.any(String), + }, + ` + Object { + "author": Object { + "name": "John Doe", + "roles": Array [ + "author", + ], + }, + "description": "testpkg", + "fingerprint": Any, + "homepage": "https://github.com/aws/jsii.git", + "jsiiVersion": Any, + "license": "Apache-2.0", + "metadata": Object { + "jsii": Object { + "pacmak": Object { + "hasDefaultInterfaces": true, + }, + }, + }, + "name": "testpkg", + "readme": Object { + "markdown": "# Missing literate source + + ## This is a heading within the literate file! + + \`\`\`java + // Example automatically generated without compilation. See https://github.com/aws/jsii/issues/826 + new SampleClass(\\"literate\\"); + \`\`\` + + # Missing fixture + + \`\`\`java + // Example automatically generated without compilation. See https://github.com/aws/jsii/issues/826 + new SampleClass(\\"README.md\\"); + \`\`\`", + }, + "repository": Object { + "type": "git", + "url": "https://github.com/aws/jsii.git", + }, + "schema": "jsii/0.10.0", + "targets": Object { + "js": Object { + "npm": "testpkg", + }, + }, + "types": Object { + "testpkg.SampleClass": Object { + "assembly": "testpkg", + "docs": Object { + "example": "// Example automatically generated without compilation. See https://github.com/aws/jsii/issues/826 + DoesNotCompile.Builder.create(this, \\"That\\").foo(1337).build();", + }, + "fqn": "testpkg.SampleClass", + "initializer": Object { + "locationInModule": Object { + "filename": "index.ts", + "line": 8, + }, + "parameters": Array [ + Object { + "name": "_source", + "type": Object { + "primitive": "string", + }, + }, + ], + }, + "kind": "class", + "locationInModule": Object { + "filename": "index.ts", + "line": 7, + }, + "name": "SampleClass", + }, + }, + "version": "0.0.1", + } + `, + ); + + expect( + fs.readJsonSync(path.join(tmpDir, `${SPEC_FILE_NAME}.python`)), + ).toMatchInlineSnapshot( + { + fingerprint: expect.any(String), + jsiiVersion: expect.any(String), + }, + ` + Object { + "author": Object { + "name": "John Doe", + "roles": Array [ + "author", + ], + }, + "description": "testpkg", + "fingerprint": Any, + "homepage": "https://github.com/aws/jsii.git", + "jsiiVersion": Any, + "license": "Apache-2.0", + "metadata": Object { + "jsii": Object { + "pacmak": Object { + "hasDefaultInterfaces": true, + }, + }, + }, + "name": "testpkg", + "readme": Object { + "markdown": "# Missing literate source + + ## This is a heading within the literate file! + + \`\`\`python + # Example automatically generated without compilation. See https://github.com/aws/jsii/issues/826 + index_1.SampleClass(\\"literate\\") + \`\`\` + + # Missing fixture + + \`\`\`python + # Example automatically generated without compilation. See https://github.com/aws/jsii/issues/826 + SampleClass(\\"README.md\\") + \`\`\`", + }, + "repository": Object { + "type": "git", + "url": "https://github.com/aws/jsii.git", + }, + "schema": "jsii/0.10.0", + "targets": Object { + "js": Object { + "npm": "testpkg", + }, + }, + "types": Object { + "testpkg.SampleClass": Object { + "assembly": "testpkg", + "docs": Object { + "example": "# Example automatically generated without compilation. See https://github.com/aws/jsii/issues/826 + DoesNotCompile(self, \\"That\\", foo=1337)", + }, + "fqn": "testpkg.SampleClass", + "initializer": Object { + "locationInModule": Object { + "filename": "index.ts", + "line": 8, + }, + "parameters": Array [ + Object { + "name": "_source", + "type": Object { + "primitive": "string", + }, + }, + ], + }, + "kind": "class", + "locationInModule": Object { + "filename": "index.ts", + "line": 7, + }, + "name": "SampleClass", + }, + }, + "version": "0.0.1", + } + `, + ); + })); + async function withTemporaryDirectory( callback: (dir: string) => Promise, ): Promise {