Skip to content

Commit a3fb51d

Browse files
authored
Generate TypesScript Library Declaration (#2016)
* Initial ts processing script * Process jsx files only * Change script name * Allow console.log in scripts folder * Pass correct file list to process * Generate declaation files for each converted file * Monitor all ts errors via preEmitDiagnostics and emitted ones * Fix gathering diagnostics * Fix ReactNode type * Disable isolatedModules option * Add api extractor * Remove flow-to-ts internal from flow checking * Inline utility types * Fix file handler event * Remove .typescript folder from flow check * Fix textConsts * Update react import * Fix react node * Fix freezed consts types * Fixes of consts/enums * Fix ts errors * Deduplicate yarn * Fix yarn lock * Fixe ts errors * Fix flow * Remove forwardRef generic params * Fix object lookup * Fix object lookup type * Fix flow and ts errors in accordion * Fix error * Generate global API declaration * Change types folder * Fix linters * Add building types to package build and CI * Remove out dir from ts cfg * Remove types dir at the begining * Fix typo in props name of accordion * Resuse tsconfig.json in typsecript compiler api * Emit declaration only * Export prop types with unique names * Fix typo
1 parent 22c31d5 commit a3fb51d

File tree

298 files changed

+2199
-2179
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

298 files changed

+2199
-2179
lines changed

.flowconfig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[ignore]
22
<PROJECT_ROOT>/node_modules/.*gulp-prettify
3-
[include]
3+
<PROJECT_ROOT>/node_modules/@khanacademy/flow-to-ts/test.json
4+
<PROJECT_ROOT>/.typescript/**
45

56
[libs]
67

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ dist/
22
esm/
33
commonjs/
44
/css/
5+
types/
56
.scss-lint.yml
67
.editorconfig
78
.eslintrc-es5
@@ -16,6 +17,8 @@ package-lock.json
1617
yarn-error.log
1718
.storybook-static
1819
.cache
20+
.typescript
21+
temp
1922

2023
# Generated images
2124
/src/images/*.js

.storybook/preview.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import * as React from 'react';
22
import {
33
Title,
44
Subtitle,

api-extractor.json

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3+
4+
"mainEntryPointFilePath": "<projectFolder>/.typescript/index.d.ts",
5+
6+
"bundledPackages": [],
7+
8+
"compiler": {},
9+
10+
"apiReport": {
11+
"enabled": false,
12+
"reportFolder": "<projectFolder>/.typescript/"
13+
},
14+
15+
"docModel": {
16+
"enabled": false,
17+
"apiJsonFilePath": "<projectFolder>/.typescript/"
18+
},
19+
20+
"dtsRollup": {
21+
"enabled": true,
22+
"untrimmedFilePath": "<projectFolder>/types/<unscopedPackageName>.d.ts"
23+
},
24+
25+
"tsdocMetadata": {},
26+
27+
"messages": {
28+
"compilerMessageReporting": {
29+
"default": {
30+
"logLevel": "warning"
31+
}
32+
},
33+
34+
"extractorMessageReporting": {
35+
"ae-missing-release-tag": {
36+
"logLevel": "none"
37+
},
38+
"default": {
39+
"logLevel": "warning"
40+
}
41+
},
42+
43+
"tsdocMessageReporting": {
44+
"default": {
45+
"logLevel": "warning"
46+
}
47+
}
48+
}
49+
}

package.json

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
"license": "GPL-3.0-only",
88
"module": "esm/index.js",
99
"main": "commonjs/index.js",
10+
"types": "types/brainly-style-guide.d.ts",
11+
"typings": "types/brainly-style-guide.d.ts",
1012
"files": [
1113
"src/",
1214
"esm/",
1315
"commonjs/",
14-
"css/"
16+
"css/",
17+
"types/"
1518
],
1619
"engines": {
1720
"node": ">=8.0.0",
@@ -32,6 +35,8 @@
3235
"@babel/preset-typescript": "^7.12.1",
3336
"@brainly/s3": "^4.4.0",
3437
"@hot-loader/react-dom": "^16.8.6",
38+
"@khanacademy/flow-to-ts": "^0.1.9",
39+
"@microsoft/api-extractor": "^7.13.0",
3540
"@storybook/addon-a11y": "@next",
3641
"@storybook/addon-actions": "@next",
3742
"@storybook/addon-docs": "@next",
@@ -49,7 +54,7 @@
4954
"del": "^2.2.0",
5055
"enzyme": "^3.1.0",
5156
"enzyme-adapter-react-16": "^1.0.1",
52-
"eslint": "^6.1.0",
57+
"eslint": "^7.18.0",
5358
"eslint-config-brainly-react": "^2.7.0",
5459
"eslint-config-prettier": "^6.0.0",
5560
"eslint-plugin-babel": "^5.3.0",
@@ -80,10 +85,12 @@
8085
"http-server": "^0.9.0",
8186
"husky": "^0.11.3",
8287
"jest": "^24.0.0",
88+
"jscodeshift": "^0.11.0",
8389
"polished": "^4.0.3",
8490
"postcss": "^8.2.2",
8591
"postcss-loader": "^4.1.0",
8692
"prettier": "^1.18.2",
93+
"progress-estimator": "^0.3.0",
8794
"query-string": "^6.5.0",
8895
"react": "^16.2.0",
8996
"react-dom": "^16.2.0",
@@ -101,6 +108,7 @@
101108
"tailwindcss": "^2.0.1",
102109
"terser-webpack-plugin": "^1.2.3",
103110
"through2": "^2.0.3",
111+
"typescript": "^4.1.3",
104112
"webpack": "^4.6.0",
105113
"webpack-cli": "^3.3.6",
106114
"webpack-dev-server": "^3.7.2",
@@ -110,20 +118,21 @@
110118
"js-lint": "yarn run eslint --ext .js --ext .jsx src/",
111119
"scss-lint": "sass-lint -c node_modules/sass-lint-config-brainly/.sass-lint.yml -v -q 'src/**/*.scss'",
112120
"scss-unused-variables": "./scripts/find_scss_unused_variables.sh src/",
113-
"js-typecheck": "flow",
121+
"js-typecheck": "flow && node ./scripts/build-types.js",
114122
"build": "gulp build",
115123
"start": "http-server ./dist -p 8000",
116124
"deploy": "gulp deploy",
117125
"postversion": "git push && git push --tags",
118126
"watch": "gulp build && gulp watch & yarn start",
119-
"test": "yarn run scss-lint && yarn run scss-unused-variables && yarn flow && yarn run js-lint && jest",
127+
"test": "yarn run scss-lint && yarn run scss-unused-variables && yarn run js-typecheck && yarn run js-lint && jest",
120128
"eslint": "./node_modules/.bin/eslint",
121129
"beta:watch": "webpack-dev-server --hot --inline --open --watch --mode=development",
122-
"package-clean": "yarn rimraf esm/ commonjs/ css/",
130+
"package-clean": "yarn rimraf esm/ commonjs/ css/ types/",
123131
"package-esm": "BABEL_ENV=esm babel src --out-dir esm --only 'src/components' --only 'src/js' --only 'src/index.js' --ignore '**/*.spec.jsx' --ignore '**/pages/**' --ignore '**/iframe-pages/**' --ignore '**/__mocks__/**'",
124132
"package-commonjs": "BABEL_ENV=commonjs babel src --out-dir commonjs --only 'src/components' --only 'src/js' --only 'src/index.js' --ignore '**/*.spec.jsx' --ignore '**/pages/**' --ignore '**/iframe-pages/**' --ignore '**/__mocks__/**'",
125133
"package-css": "node ./scripts/build-css.js",
126-
"package-prepare": "yarn package-clean && concurrently --names 'commonjs,esm,css' 'yarn run package-commonjs' 'yarn run package-esm' 'yarn run package-css'",
134+
"package-types": "node ./scripts/build-types.js",
135+
"package-prepare": "yarn package-clean && concurrently --names 'commonjs,esm,css' 'yarn run package-commonjs' 'yarn run package-esm' 'yarn run package-css' 'yarn run package-types'",
127136
"prepublishOnly": "yarn package-prepare",
128137
"storybook": "start-storybook -p 6006 -s ./.storybook/public",
129138
"storybook-docs": "start-storybook --docs -p 6006 -s ./.storybook/public",

scripts/.eslintrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"rules": {
3+
"no-console": "off"
4+
}
5+
}

scripts/build-types.js

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#!/usr/bin/env node
2+
3+
'use strict';
4+
5+
const glob = require('glob');
6+
const path = require('path');
7+
const fs = require('fs-extra');
8+
const jsc = require('jscodeshift');
9+
const flowParser = require('jscodeshift/parser/flow');
10+
const convert = require('@khanacademy/flow-to-ts/src/convert');
11+
const ts = require('typescript');
12+
const apiExtractor = require('@microsoft/api-extractor');
13+
const tsConfig = require('../tsconfig.json');
14+
15+
const ROOT_DIR = path.resolve(__dirname, '../');
16+
const SOURCE_DIR = path.join(ROOT_DIR, 'src');
17+
const DEST_DIR = path.join(ROOT_DIR, '.typescript');
18+
const TYPES_DIR = path.join(ROOT_DIR, 'types');
19+
20+
const files = glob.sync(
21+
`{/components/**/*.{js,jsx},/js/generateRandomString.js,/index.js}`,
22+
{
23+
ignore: [`/**/{pages,iframe-pages,__mocks__}/*`, '/**/*{stories,spec}.*'],
24+
root: SOURCE_DIR,
25+
}
26+
);
27+
28+
console.log(`Found ${files.length} source files.`);
29+
30+
fs.removeSync(DEST_DIR);
31+
fs.removeSync(TYPES_DIR);
32+
33+
files.forEach(sourceFile => {
34+
const flowCode = fs.readFileSync(sourceFile, 'utf-8');
35+
36+
const ast = jsc(flowCode, {
37+
parser: flowParser(),
38+
});
39+
40+
let transformedCode = ast.toSource();
41+
42+
// Code transformations before converting to typescript
43+
// to remove incompatibilities weren't caught by flow-to-ts
44+
45+
// Remove generic parameters from forwardRef as Flow uses opposite order
46+
// forwardRef<Prop, Element> -> forwardRef
47+
if (ast.find(jsc.Identifier, {name: 'forwardRef'}).length) {
48+
transformedCode = transformedCode.replace(/forwardRef<.*>/g, 'forwardRef');
49+
}
50+
51+
const typescriptCode = convert(transformedCode, {
52+
printWidth: 80,
53+
singleQuote: true,
54+
semi: false,
55+
prettier: true,
56+
inlineUtilityTypes: true,
57+
});
58+
59+
const sourceExtension = path.extname(sourceFile);
60+
const destinationExtension = mapExtension(sourceExtension);
61+
62+
const relativeSourceFile = path.relative(SOURCE_DIR, sourceFile);
63+
const outputFile = path.join(
64+
DEST_DIR,
65+
relativeSourceFile.replace(sourceExtension, destinationExtension)
66+
);
67+
68+
fs.outputFileSync(outputFile, typescriptCode, noop => noop);
69+
});
70+
71+
const tsFiles = glob.sync('.typescript/**/*.{ts,tsx}');
72+
73+
const options = {...tsConfig.compilerOptions, declarationDir: '.typescript'};
74+
75+
console.log('Generating declaration files...');
76+
77+
const program = ts.createProgram(tsFiles, options);
78+
79+
const res = program.emit();
80+
81+
const allDiagnostics = ts
82+
.getPreEmitDiagnostics(program)
83+
.concat(res.diagnostics);
84+
85+
allDiagnostics.forEach(diagnostics => {
86+
if (diagnostics.file) {
87+
const {line, character} = diagnostics.file.getLineAndCharacterOfPosition(
88+
diagnostics.start
89+
);
90+
const message = ts.flattenDiagnosticMessageText(
91+
diagnostics.messageText,
92+
'\n'
93+
);
94+
95+
console.log(
96+
`${diagnostics.file.fileName} (${line + 1}, ${character + 1}): ${message}`
97+
);
98+
} else {
99+
console.log(ts.flattenDiagnosticMessageText(diagnostics.messageText, '\n'));
100+
}
101+
});
102+
103+
console.log(
104+
`Found ${allDiagnostics.length > 0 ? allDiagnostics.length : 'No'} errors.`
105+
);
106+
107+
// Extract API
108+
// Combine all d.ts files into one library.d.ts
109+
110+
console.log('Generating library API declaration...');
111+
112+
const apiExtractorJsonPath = path.join(ROOT_DIR, 'api-extractor.json');
113+
114+
const extractorConfig = apiExtractor.ExtractorConfig.loadFileAndPrepare(
115+
apiExtractorJsonPath
116+
);
117+
118+
fs.ensureDirSync(TYPES_DIR);
119+
120+
const extractorResult = apiExtractor.Extractor.invoke(extractorConfig, {
121+
localBuild: true,
122+
showVerboseMessages: true,
123+
});
124+
125+
if (extractorResult.succeeded) {
126+
console.log(`API Extractor completed successfully`);
127+
process.exitCode = 0;
128+
} else {
129+
console.error(
130+
`API Extractor completed with ${extractorResult.errorCount} errors` +
131+
` and ${extractorResult.warningCount} warnings`
132+
);
133+
process.exitCode = 1;
134+
}
135+
136+
function mapExtension(extension = '') {
137+
const map = {
138+
'.js': '.ts',
139+
'.jsx': '.tsx',
140+
};
141+
142+
const ext = map[extension];
143+
144+
if (!ext) {
145+
throw new Error(
146+
`Extension '${extension}' doesn't have matching element in map.`
147+
);
148+
}
149+
150+
return ext;
151+
}

src/_docs/blocks/DocsStory.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @flow
22

3-
import React from 'react';
3+
import * as React from 'react';
44
import deprecate from 'util-deprecate';
55
import dedent from 'ts-dedent';
66
import {

src/_docs/blocks/IconGallery.jsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// @flow
22

3-
import React from 'react';
4-
import type {Node} from 'react';
3+
import * as React from 'react';
54
import {styled} from '@storybook/theming';
65
import {ResetWrapper} from '@storybook/components/dist/typography/DocumentFormatting';
76

@@ -43,7 +42,7 @@ const List = styled.div({
4342
interface IconItemProps {
4443
name: string;
4544
size: number;
46-
children: Node;
45+
children: React.Node;
4746
}
4847

4948
export const IconItem = ({name, size, children}: IconItemProps) => (
@@ -54,7 +53,7 @@ export const IconItem = ({name, size, children}: IconItemProps) => (
5453
);
5554

5655
interface IconGalleryProps {
57-
children: Node;
56+
children: React.Node;
5857
}
5958

6059
export const IconGallery = ({children, ...props}: IconGalleryProps) => (

0 commit comments

Comments
 (0)