Skip to content

Commit

Permalink
feat: use typescript virtual file server to allow transpilation witho…
Browse files Browse the repository at this point in the history
…ut requiring a real fs (#268)
  • Loading branch information
alharris-at authored Dec 2, 2021
1 parent 04c3baa commit d8219c5
Show file tree
Hide file tree
Showing 8 changed files with 357 additions and 98 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`react-studio-template-renderer-helper transpile fails to transpile with ScriptTarget ESNext 1`] = `"ScriptTarget 99 not supported with type declarations enabled, expected one of [0,1,2,3,4,5,6,7,8]"`;

exports[`react-studio-template-renderer-helper transpile fails to transpile with ScriptTarget Latest 1`] = `"ScriptTarget 99 not supported with type declarations enabled, expected one of [0,1,2,3,4,5,6,7,8]"`;

exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES3 1`] = `
"import React from \\"react\\";
import { ViewTestProps } from \\"./ViewTest\\";
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
overrides?: EscapeHatchProps | undefined | null;
}>;
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
"
`;
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES5 1`] = `
"import React from \\"react\\";
import { ViewTestProps } from \\"./ViewTest\\";
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
overrides?: EscapeHatchProps | undefined | null;
}>;
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
"
`;
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2015 1`] = `
"import React from \\"react\\";
import { ViewTestProps } from \\"./ViewTest\\";
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
overrides?: EscapeHatchProps | undefined | null;
}>;
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
"
`;
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2016 1`] = `
"import React from \\"react\\";
import { ViewTestProps } from \\"./ViewTest\\";
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
overrides?: EscapeHatchProps | undefined | null;
}>;
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
"
`;
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2017 1`] = `
"import React from \\"react\\";
import { ViewTestProps } from \\"./ViewTest\\";
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
overrides?: EscapeHatchProps | undefined | null;
}>;
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
"
`;
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2018 1`] = `
"import React from \\"react\\";
import { ViewTestProps } from \\"./ViewTest\\";
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
overrides?: EscapeHatchProps | undefined | null;
}>;
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
"
`;
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2019 1`] = `
"import React from \\"react\\";
import { ViewTestProps } from \\"./ViewTest\\";
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
overrides?: EscapeHatchProps | undefined | null;
}>;
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
"
`;
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2020 1`] = `
"import React from \\"react\\";
import { ViewTestProps } from \\"./ViewTest\\";
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
overrides?: EscapeHatchProps | undefined | null;
}>;
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
"
`;
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2021 1`] = `
"import React from \\"react\\";
import { ViewTestProps } from \\"./ViewTest\\";
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
overrides?: EscapeHatchProps | undefined | null;
}>;
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License").
You may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { StudioTemplateRendererFactory, StudioComponent } from '@aws-amplify/codegen-ui';
import fs from 'fs';
import { join } from 'path';
import { ScriptTarget, ScriptKind } from '..';
import { AmplifyRenderer } from '../amplify-ui-renderers/amplify-renderer';

function loadSchemaFromJSONFile(jsonSchemaFile: string): StudioComponent {
return JSON.parse(
fs.readFileSync(join(__dirname, 'studio-ui-json', `${jsonSchemaFile}.json`), 'utf-8'),
) as StudioComponent;
}

function generateDeclarationForScriptTarget(jsonSchemaFile: string, target: ScriptTarget): any {
const rendererFactory = new StudioTemplateRendererFactory(
(component: StudioComponent) =>
new AmplifyRenderer(component, { target, script: ScriptKind.JS, renderTypeDeclarations: true }),
);
const component = rendererFactory.buildRenderer(loadSchemaFromJSONFile(jsonSchemaFile)).renderComponent();
return (component as unknown as { declaration: string }).declaration;
}

describe('react-studio-template-renderer-helper', () => {
describe('transpile', () => {
it('successfully transpiles with ScriptTarget ES3', () => {
expect(generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES3)).toMatchSnapshot();
});

it('successfully transpiles with ScriptTarget ES5', () => {
expect(generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES5)).toMatchSnapshot();
});

it('successfully transpiles with ScriptTarget ES2015', () => {
expect(
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES2015),
).toMatchSnapshot();
});

it('successfully transpiles with ScriptTarget ES2016', () => {
expect(
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES2016),
).toMatchSnapshot();
});

it('successfully transpiles with ScriptTarget ES2017', () => {
expect(
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES2017),
).toMatchSnapshot();
});

it('successfully transpiles with ScriptTarget ES2018', () => {
expect(
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES2018),
).toMatchSnapshot();
});

it('successfully transpiles with ScriptTarget ES2019', () => {
expect(
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES2019),
).toMatchSnapshot();
});

it('successfully transpiles with ScriptTarget ES2020', () => {
expect(
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES2020),
).toMatchSnapshot();
});

it('successfully transpiles with ScriptTarget ES2021', () => {
expect(
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES2021),
).toMatchSnapshot();
});

it('fails to transpile with ScriptTarget ESNext', () => {
expect(() => {
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ESNext);
}).toThrowErrorMatchingSnapshot();
});

it('fails to transpile with ScriptTarget Latest', () => {
expect(() => {
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.Latest);
}).toThrowErrorMatchingSnapshot();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import {
isDataPropertyBinding,
StudioComponentDataPropertyBinding,
StudioComponentSimplePropertyBinding,
InternalError,
InvalidInputError,
} from '@aws-amplify/codegen-ui';

import ts, {
createPrinter,
createSourceFile,
Expand All @@ -33,11 +34,10 @@ import ts, {
ObjectLiteralExpression,
createProgram,
} from 'typescript';
import { createDefaultMapFromNodeModules, createSystem, createVirtualCompilerHost } from '@typescript/vfs';
import prettier from 'prettier';
import parserTypescript from 'prettier/parser-typescript';
import fs from 'fs';
import path from 'path';
import temp from 'temp';
import { ReactRenderConfig, ScriptKind, ScriptTarget, ModuleKind } from './react-render-config';

export const defaultRenderConfig = {
Expand All @@ -46,6 +46,18 @@ export const defaultRenderConfig = {
module: ModuleKind.ESNext,
};

const supportedTranspilationTargets = [
ScriptTarget.ES3,
ScriptTarget.ES5,
ScriptTarget.ES2015,
ScriptTarget.ES2016,
ScriptTarget.ES2017,
ScriptTarget.ES2018,
ScriptTarget.ES2019,
ScriptTarget.ES2020,
ScriptTarget.ES2021,
];

export function transpile(
code: string,
renderConfig: ReactRenderConfig,
Expand All @@ -63,33 +75,45 @@ export function transpile(

const componentText = prettier.format(transpiledCode, { parser: 'typescript', plugins: [parserTypescript] });

/* createProgram is less performant than traspileModule and should only be used when necessary.
/*
* createProgram is less performant than traspileModule and should only be used when necessary.
* createProgram is used here becuase transpileModule cannot produce type declarations.
* We execute in a virtual filesystem to ensure we have no dependencies on platform fs in this stage.
*/
if (renderTypeDeclarations) {
temp.track(); // tracks temp resources created to then be deleted by temp.cleanupSync
if (target && !new Set(supportedTranspilationTargets).has(target)) {
throw new InvalidInputError(
`ScriptTarget ${target} not supported with type declarations enabled, expected one of ${JSON.stringify(
supportedTranspilationTargets,
)}`,
);
}

try {
const tmpFile = temp.openSync({ suffix: '.tsx' });
const tmpDir = temp.mkdirSync();
const compilerOptions = {
target,
module,
declaration: true,
emitDeclarationOnly: true,
skipLibCheck: true,
};

fs.writeFileSync(tmpFile.path, code);
const fsMap = createDefaultMapFromNodeModules(compilerOptions, ts);
fsMap.set('index.tsx', code);

createProgram([tmpFile.path], {
target,
module,
declaration: true,
emitDeclarationOnly: true,
outDir: tmpDir,
skipLibCheck: true,
}).emit();
const host = createVirtualCompilerHost(createSystem(fsMap), compilerOptions, ts);
createProgram({
rootNames: [...fsMap.keys()],
options: compilerOptions,
host: host.compilerHost,
}).emit();

const declaration = fs.readFileSync(path.join(tmpDir, getDeclarationFilename(tmpFile.path)), 'utf8');
const declaration = fsMap.get('index.d.ts');

return { componentText, declaration };
} finally {
temp.cleanupSync();
if (!declaration) {
throw new InternalError('Component declaration file not generated');
}

return { componentText, declaration };
}

return {
Expand Down
Loading

0 comments on commit d8219c5

Please sign in to comment.