Skip to content

Commit d8219c5

Browse files
authored
feat: use typescript virtual file server to allow transpilation without requiring a real fs (#268)
1 parent 04c3baa commit d8219c5

File tree

8 files changed

+357
-98
lines changed

8 files changed

+357
-98
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
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]"`;
4+
5+
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]"`;
6+
7+
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES3 1`] = `
8+
"import React from \\"react\\";
9+
import { ViewTestProps } from \\"./ViewTest\\";
10+
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
11+
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
12+
overrides?: EscapeHatchProps | undefined | null;
13+
}>;
14+
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
15+
"
16+
`;
17+
18+
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES5 1`] = `
19+
"import React from \\"react\\";
20+
import { ViewTestProps } from \\"./ViewTest\\";
21+
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
22+
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
23+
overrides?: EscapeHatchProps | undefined | null;
24+
}>;
25+
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
26+
"
27+
`;
28+
29+
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2015 1`] = `
30+
"import React from \\"react\\";
31+
import { ViewTestProps } from \\"./ViewTest\\";
32+
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
33+
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
34+
overrides?: EscapeHatchProps | undefined | null;
35+
}>;
36+
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
37+
"
38+
`;
39+
40+
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2016 1`] = `
41+
"import React from \\"react\\";
42+
import { ViewTestProps } from \\"./ViewTest\\";
43+
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
44+
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
45+
overrides?: EscapeHatchProps | undefined | null;
46+
}>;
47+
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
48+
"
49+
`;
50+
51+
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2017 1`] = `
52+
"import React from \\"react\\";
53+
import { ViewTestProps } from \\"./ViewTest\\";
54+
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
55+
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
56+
overrides?: EscapeHatchProps | undefined | null;
57+
}>;
58+
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
59+
"
60+
`;
61+
62+
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2018 1`] = `
63+
"import React from \\"react\\";
64+
import { ViewTestProps } from \\"./ViewTest\\";
65+
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
66+
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
67+
overrides?: EscapeHatchProps | undefined | null;
68+
}>;
69+
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
70+
"
71+
`;
72+
73+
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2019 1`] = `
74+
"import React from \\"react\\";
75+
import { ViewTestProps } from \\"./ViewTest\\";
76+
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
77+
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
78+
overrides?: EscapeHatchProps | undefined | null;
79+
}>;
80+
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
81+
"
82+
`;
83+
84+
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2020 1`] = `
85+
"import React from \\"react\\";
86+
import { ViewTestProps } from \\"./ViewTest\\";
87+
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
88+
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
89+
overrides?: EscapeHatchProps | undefined | null;
90+
}>;
91+
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
92+
"
93+
`;
94+
95+
exports[`react-studio-template-renderer-helper transpile successfully transpiles with ScriptTarget ES2021 1`] = `
96+
"import React from \\"react\\";
97+
import { ViewTestProps } from \\"./ViewTest\\";
98+
import { EscapeHatchProps } from \\"@aws-amplify/ui-react\\";
99+
export declare type CustomParentAndChildrenProps = React.PropsWithChildren<Partial<ViewTestProps> & {
100+
overrides?: EscapeHatchProps | undefined | null;
101+
}>;
102+
export default function CustomParentAndChildren(props: CustomParentAndChildrenProps): React.ReactElement;
103+
"
104+
`;
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License").
5+
You may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
import { StudioTemplateRendererFactory, StudioComponent } from '@aws-amplify/codegen-ui';
17+
import fs from 'fs';
18+
import { join } from 'path';
19+
import { ScriptTarget, ScriptKind } from '..';
20+
import { AmplifyRenderer } from '../amplify-ui-renderers/amplify-renderer';
21+
22+
function loadSchemaFromJSONFile(jsonSchemaFile: string): StudioComponent {
23+
return JSON.parse(
24+
fs.readFileSync(join(__dirname, 'studio-ui-json', `${jsonSchemaFile}.json`), 'utf-8'),
25+
) as StudioComponent;
26+
}
27+
28+
function generateDeclarationForScriptTarget(jsonSchemaFile: string, target: ScriptTarget): any {
29+
const rendererFactory = new StudioTemplateRendererFactory(
30+
(component: StudioComponent) =>
31+
new AmplifyRenderer(component, { target, script: ScriptKind.JS, renderTypeDeclarations: true }),
32+
);
33+
const component = rendererFactory.buildRenderer(loadSchemaFromJSONFile(jsonSchemaFile)).renderComponent();
34+
return (component as unknown as { declaration: string }).declaration;
35+
}
36+
37+
describe('react-studio-template-renderer-helper', () => {
38+
describe('transpile', () => {
39+
it('successfully transpiles with ScriptTarget ES3', () => {
40+
expect(generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES3)).toMatchSnapshot();
41+
});
42+
43+
it('successfully transpiles with ScriptTarget ES5', () => {
44+
expect(generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES5)).toMatchSnapshot();
45+
});
46+
47+
it('successfully transpiles with ScriptTarget ES2015', () => {
48+
expect(
49+
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES2015),
50+
).toMatchSnapshot();
51+
});
52+
53+
it('successfully transpiles with ScriptTarget ES2016', () => {
54+
expect(
55+
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES2016),
56+
).toMatchSnapshot();
57+
});
58+
59+
it('successfully transpiles with ScriptTarget ES2017', () => {
60+
expect(
61+
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES2017),
62+
).toMatchSnapshot();
63+
});
64+
65+
it('successfully transpiles with ScriptTarget ES2018', () => {
66+
expect(
67+
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES2018),
68+
).toMatchSnapshot();
69+
});
70+
71+
it('successfully transpiles with ScriptTarget ES2019', () => {
72+
expect(
73+
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES2019),
74+
).toMatchSnapshot();
75+
});
76+
77+
it('successfully transpiles with ScriptTarget ES2020', () => {
78+
expect(
79+
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES2020),
80+
).toMatchSnapshot();
81+
});
82+
83+
it('successfully transpiles with ScriptTarget ES2021', () => {
84+
expect(
85+
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ES2021),
86+
).toMatchSnapshot();
87+
});
88+
89+
it('fails to transpile with ScriptTarget ESNext', () => {
90+
expect(() => {
91+
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.ESNext);
92+
}).toThrowErrorMatchingSnapshot();
93+
});
94+
95+
it('fails to transpile with ScriptTarget Latest', () => {
96+
expect(() => {
97+
generateDeclarationForScriptTarget('custom/customParentAndChildren', ScriptTarget.Latest);
98+
}).toThrowErrorMatchingSnapshot();
99+
});
100+
});
101+
});

packages/codegen-ui-react/lib/react-studio-template-renderer-helper.ts

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ import {
1717
isDataPropertyBinding,
1818
StudioComponentDataPropertyBinding,
1919
StudioComponentSimplePropertyBinding,
20+
InternalError,
21+
InvalidInputError,
2022
} from '@aws-amplify/codegen-ui';
21-
2223
import ts, {
2324
createPrinter,
2425
createSourceFile,
@@ -33,11 +34,10 @@ import ts, {
3334
ObjectLiteralExpression,
3435
createProgram,
3536
} from 'typescript';
37+
import { createDefaultMapFromNodeModules, createSystem, createVirtualCompilerHost } from '@typescript/vfs';
3638
import prettier from 'prettier';
3739
import parserTypescript from 'prettier/parser-typescript';
38-
import fs from 'fs';
3940
import path from 'path';
40-
import temp from 'temp';
4141
import { ReactRenderConfig, ScriptKind, ScriptTarget, ModuleKind } from './react-render-config';
4242

4343
export const defaultRenderConfig = {
@@ -46,6 +46,18 @@ export const defaultRenderConfig = {
4646
module: ModuleKind.ESNext,
4747
};
4848

49+
const supportedTranspilationTargets = [
50+
ScriptTarget.ES3,
51+
ScriptTarget.ES5,
52+
ScriptTarget.ES2015,
53+
ScriptTarget.ES2016,
54+
ScriptTarget.ES2017,
55+
ScriptTarget.ES2018,
56+
ScriptTarget.ES2019,
57+
ScriptTarget.ES2020,
58+
ScriptTarget.ES2021,
59+
];
60+
4961
export function transpile(
5062
code: string,
5163
renderConfig: ReactRenderConfig,
@@ -63,33 +75,45 @@ export function transpile(
6375

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

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

72-
try {
73-
const tmpFile = temp.openSync({ suffix: '.tsx' });
74-
const tmpDir = temp.mkdirSync();
92+
const compilerOptions = {
93+
target,
94+
module,
95+
declaration: true,
96+
emitDeclarationOnly: true,
97+
skipLibCheck: true,
98+
};
7599

76-
fs.writeFileSync(tmpFile.path, code);
100+
const fsMap = createDefaultMapFromNodeModules(compilerOptions, ts);
101+
fsMap.set('index.tsx', code);
77102

78-
createProgram([tmpFile.path], {
79-
target,
80-
module,
81-
declaration: true,
82-
emitDeclarationOnly: true,
83-
outDir: tmpDir,
84-
skipLibCheck: true,
85-
}).emit();
103+
const host = createVirtualCompilerHost(createSystem(fsMap), compilerOptions, ts);
104+
createProgram({
105+
rootNames: [...fsMap.keys()],
106+
options: compilerOptions,
107+
host: host.compilerHost,
108+
}).emit();
86109

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

89-
return { componentText, declaration };
90-
} finally {
91-
temp.cleanupSync();
112+
if (!declaration) {
113+
throw new InternalError('Component declaration file not generated');
92114
}
115+
116+
return { componentText, declaration };
93117
}
94118

95119
return {

0 commit comments

Comments
 (0)