diff --git a/lib/octicons_react/package.json b/lib/octicons_react/package.json index a438dfdcb..5b7edd1a9 100644 --- a/lib/octicons_react/package.json +++ b/lib/octicons_react/package.json @@ -7,13 +7,15 @@ "license": "MIT", "main": "dist/index.umd.js", "module": "dist/index.esm.js", + "types": "dist/index.d.ts", "repository": "primer/octicons", "scripts": { - "pretest": "npm run lint", + "pretest": "npm run lint && npm run ts-test", + "ts-test": "tsc -P ts-tests", "test": "jest", "start": "NODE_ENV=production next", "lint": "eslint src pages script", - "prepare": "script/build.js && npm run rollup", + "prepare": "script/build.js && script/types.js && npm run rollup", "prepublishOnly": "../../script/notify pending", "preversion": "npm run prepare", "publish": "../../script/notify success", @@ -31,6 +33,7 @@ "prop-types": "^15.6.1" }, "devDependencies": { + "@types/react": "^16.4.6", "babel-preset-env": "^1.7.0", "babel-preset-react": "^6.24.1", "babel-preset-stage-0": "^6.24.1", @@ -47,7 +50,8 @@ "react-test-renderer": "^16.4.1", "rollup": "^0.62.0", "rollup-plugin-babel": "^3.0.5", - "rollup-plugin-commonjs": "^9.1.3" + "rollup-plugin-commonjs": "^9.1.3", + "typescript": "^2.9.2" }, "peerDependencies": { "react": ">=15" diff --git a/lib/octicons_react/script/build.js b/lib/octicons_react/script/build.js index 8e03a696f..10d9276ce 100755 --- a/lib/octicons_react/script/build.js +++ b/lib/octicons_react/script/build.js @@ -5,17 +5,21 @@ const {join, resolve} = require('path') const srcDir = resolve(__dirname, '../src/__generated__') const iconsFile = join(srcDir, 'icons.js') +const typesFile = join(srcDir, 'icons.d.ts') function CamelCase(str) { return str.replace(/(^|-)([a-z])/g, (_, __, c) => c.toUpperCase()) } -const icons = [...Object.entries(octicons)] +const octiconNames = [...Object.entries(octicons)] + +const icons = octiconNames .map(([key, octicon]) => { const name = CamelCase(key) const {width, height, path} = octicon // convert attributes like fill-rule into JSX equivalents, e.g. fillRule const svg = path.replace(/([a-z]+)-([a-z]+)=/g, (_, a, b) => `${a}${CamelCase(b)}=`) + const type = `Icon<${width}, ${height}>` const code = `function ${name}() { return ${svg} } @@ -26,6 +30,7 @@ ${name}.size = [${width}, ${height}] key, name, octicon, + type, code } }) @@ -57,9 +62,44 @@ export { }) } +function writeTypes(file) { + const count = icons.length + const code = `/* THIS FILE IS GENERATED. DO NOT EDIT IT. */ +import * as React from 'react' + +type Icon< + W extends number = number, + H extends number = number +> = React.SFC<{}> & { size: [W, H] }; + +${icons.map(({name, type}) => `declare const ${name}: ${type}`).join('\n')} + +type iconsByName = { + ${icons.map(({key, type}) => `'${key}': ${type}`).join(',\n ')} +} +declare const iconsByName: iconsByName + +declare function getIconByName( + name: T +): IconsByName[T]; +declare function getIconByName(name: string): Icon | undefined + +export { + Icon, + getIconByName, + iconsByName, + ${icons.map(({name}) => name).join(',\n ')} +}` + return fse.writeFile(file, code, 'utf8').then(() => { + console.warn('wrote %s with %d exports', file, count) + return icons + }) +} + fse .mkdirs(srcDir) .then(() => writeIcons(iconsFile)) + .then(() => writeTypes(typesFile)) .catch(error => { console.error(error) process.exit(1) diff --git a/lib/octicons_react/script/types.js b/lib/octicons_react/script/types.js new file mode 100755 index 000000000..30af4ed38 --- /dev/null +++ b/lib/octicons_react/script/types.js @@ -0,0 +1,23 @@ +#!/usr/bin/env node +const fse = require('fs-extra') +const {join, resolve} = require('path') + +const srcDir = resolve(__dirname, '../src/__generated__') +const iconsSrc = join(srcDir, 'icons.d.ts') +const indexSrc = join(srcDir, '../index.d.ts') + +const destDir = resolve(__dirname, '../dist') +const iconsDest = join(destDir, 'icons.d.ts') +const indexDest = join(destDir, 'index.d.ts') + +fse.copy(iconsSrc, iconsDest).catch(die) +fse + .readFile(indexSrc, 'utf8') + .then(content => content.replace(/.\/__generated__\//g, './')) + .then(fse.writeFile.bind(fse, indexDest)) + .catch(die) + +function die(err) { + console.error(err.stack) + process.exit(1) +} diff --git a/lib/octicons_react/src/index.d.ts b/lib/octicons_react/src/index.d.ts new file mode 100644 index 000000000..790a54c5e --- /dev/null +++ b/lib/octicons_react/src/index.d.ts @@ -0,0 +1,24 @@ +import * as React from 'react' + +import {Icon} from './__generated__/icons' + +type Size = 'small' | 'medium' | 'large' +export interface OcticonProps { + ariaLabel?: string + children?: React.ReactElement + height?: number + icon: Icon + size?: number | Size + verticalAlign?: 'middle' | 'text-bottom' | 'text-top' | 'top' + width?: number +} + +declare const Octicon: React.SFC +export default Octicon + +export function createIcon, W extends number, H extends number>( + component: C, + size: [W, H] +): Icon + +export * from './__generated__/icons' diff --git a/lib/octicons_react/src/index.js b/lib/octicons_react/src/index.js index 87f1dfa17..189e2e6cb 100644 --- a/lib/octicons_react/src/index.js +++ b/lib/octicons_react/src/index.js @@ -66,4 +66,10 @@ Octicon.propTypes = { width: PropTypes.number } +// Helper since TS makes this painful +export function createIcon(component, size) { + component.size = size + return component +} + export * from './__generated__/icons' diff --git a/lib/octicons_react/ts-tests/index.tsx b/lib/octicons_react/ts-tests/index.tsx new file mode 100644 index 000000000..15fc3043d --- /dev/null +++ b/lib/octicons_react/ts-tests/index.tsx @@ -0,0 +1,67 @@ +import * as React from 'react' +import Octicon, { + OcticonProps, + Beaker, + Zap, + Repo, + Plus, + LogoGithub, + getIconByName, + iconsByName, + createIcon +} from '../src' + +function Icon({boom}: {boom: boolean}): React.ReactNode { + return +} + +function OcticonByName({name, ...props}: {name: keyof iconsByName} & OcticonProps): React.ReactNode { + return +} + +// Unfortunately, `Object.keys` returns `string[]` unconditionally; +// see https://github.com/Microsoft/TypeScript/pull/13971 & +// https://github.com/Microsoft/TypeScript/issues/12870 for details. +function keys(obj: T): (keyof T)[] { + return Object.keys(obj) as (keyof T)[] +} + +function OcticonsList() { + return ( +
    + {keys(iconsByName).map(key => ( +
  • + {key} + +
  • + ))} +
+ ) +} + +function VerticalAlign() { + return ( +

+ github/github + New + +

+ ) +} + +const CirclesIcon = createIcon( + () => { + return ( + + + + + + ) + }, + [30, 10] +) + +export function CirclesOcticon(props: OcticonProps) { + return +} diff --git a/lib/octicons_react/ts-tests/tsconfig.json b/lib/octicons_react/ts-tests/tsconfig.json new file mode 100644 index 000000000..74e387903 --- /dev/null +++ b/lib/octicons_react/ts-tests/tsconfig.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "compileOnSave": false, + "compilerOptions": { + "module": "commonjs", + "noEmit": true, + "noImplicitAny": true, + "jsx": "react", + "lib": ["es2015"] + }, + "files": ["../src/index.d.ts", "./index.tsx"] +}