From 7a9b41c51783fd3cfe7418e71f9dd19ccae722c2 Mon Sep 17 00:00:00 2001 From: Andrew Aylett Date: Sun, 26 Jul 2020 15:30:03 +0100 Subject: [PATCH] Generate type objects for io-ts This allows easy compatibility between objects type checked by io-ts and (for example) React's use of csstype to define typed interfaces. --- build.ts | 15 ++++- package.json | 10 ++- src/declarator.ts | 2 + src/output.ts | 151 +++++++++++++++++++++++++++++++++++++++++++++- utils.ts | 1 + yarn.lock | 44 +++----------- 6 files changed, 181 insertions(+), 42 deletions(-) diff --git a/build.ts b/build.ts index 87fd1d3..0ed232b 100644 --- a/build.ts +++ b/build.ts @@ -1,7 +1,7 @@ import * as chokidar from 'chokidar'; import * as path from 'path'; import * as prettier from 'prettier'; -import { FLOW_FILENAME, spawnAsync, TYPESCRIPT_FILENAME, writeFileAsync } from './utils'; +import { FLOW_FILENAME, IOTS_FILENAME, spawnAsync, TYPESCRIPT_FILENAME, writeFileAsync } from './utils'; const ROOT_DIR = __dirname; const TEST_FILENAME = 'typecheck.ts'; @@ -42,9 +42,17 @@ export default async function trigger() { console.info('Generating...'); const output = await create(); console.info('Formatting...'); - const [flow, typescript] = await Promise.all([format(output.flow, 'flow'), format(output.typescript, 'typescript')]); + const [flow, typescript, iots] = await Promise.all([ + format(output.flow, 'flow'), + format(output.typescript, 'typescript'), + format(output.iots, 'typescript'), + ],); console.info(`Writing files...`); - await Promise.all([writeFileAsync(FLOW_FILENAME, flow), writeFileAsync(TYPESCRIPT_FILENAME, typescript)]); + await Promise.all([ + writeFileAsync(FLOW_FILENAME, flow), + writeFileAsync(TYPESCRIPT_FILENAME, typescript), + writeFileAsync(IOTS_FILENAME, iots), + ],); console.info('Type checking...'); await typecheck(); } @@ -79,6 +87,7 @@ function typecheck() { spawnAsync( path.join(ROOT_DIR, `node_modules/.bin/${process.platform === 'win32' ? 'tsc.cmd' : 'tsc'}`), path.join(ROOT_DIR, TYPESCRIPT_FILENAME), + path.join(ROOT_DIR, IOTS_FILENAME), path.join(ROOT_DIR, TEST_FILENAME), '--noEmit', ), diff --git a/package.json b/package.json index 15340e8..fda42e8 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "author": "Fredrik Nicol ", "license": "MIT", "devDependencies": { - "@types/chokidar": "^2.1.3", "@types/jest": "^24.0.21", "@types/jsdom": "^12.2.4", "@types/node": "^12.12.3", @@ -43,7 +42,8 @@ }, "files": [ "index.d.ts", - "index.js.flow" + "index.js.flow", + "csstype.ts" ], "keywords": [ "css", @@ -53,5 +53,9 @@ "typings", "types", "definitions" - ] + ], + "dependencies": { + "fp-ts": "^2.7.1", + "io-ts": "^2.2.9" + } } diff --git a/src/declarator.ts b/src/declarator.ts index cfe1214..e99f4d9 100644 --- a/src/declarator.ts +++ b/src/declarator.ts @@ -15,6 +15,7 @@ export interface IAlias { export interface IGenerics { name: string; defaults?: string; + default_types?: string; } interface Interface { @@ -147,6 +148,7 @@ declarations.set(declarableGlobalsAndNumber, globalsAndNumberDeclaration); export const lengthGeneric: IGenerics = { name: 'TLength', defaults: 'string | 0', + default_types: 't.union([t.string, t.literal(0)])' }; const standardLonghandPropertiesDefinition: IPropertyAlias[] = []; diff --git a/src/output.ts b/src/output.ts index f20c281..b7e11f6 100644 --- a/src/output.ts +++ b/src/output.ts @@ -1,11 +1,12 @@ -import { DeclarableType, declarations, IGenerics, interfaces, isAliasProperty } from './declarator'; -import { Type } from './typer'; +import { DeclarableType, declarations, IDeclaration, IGenerics, interfaces, isAliasProperty } from './declarator'; +import { Type} from './typer'; const EOL = '\n'; export default () => ({ flow: flow(), typescript: typescript(), + iots: iots(), }); function flow() { @@ -128,6 +129,121 @@ function typescript() { return interfacesOutput + EOL + EOL + declarationsOutput; } +function iots() { + let header = 'import * as t from \'io-ts\''; + let interfacesOutput = ''; + let genericsOutput = ''; + const generics: Record = {}; + for (const item of interfaces) { + interfacesOutput += EOL + EOL; + + for (const generic of item.generics) { + generics[generic.name] = generic; + } + + interfacesOutput += `export const ${item.name} = `; + + if (item.extends.length > 0) { + interfacesOutput += 't.intersection([' + for (const extend of item.extends) { + interfacesOutput += `${extend.name},`; + } + } + + if (item.properties.length > 0) { + interfacesOutput += `t.partial({` + for (const property of item.properties) { + interfacesOutput += EOL; + if (property.comment) { + interfacesOutput += property.comment + EOL; + } + + if (isAliasProperty(property)) { + const value = property.alias.name + interfacesOutput += `${JSON.stringify(property.name)}: ${ + item.fallback ? `t.union([${value}, t.array(${value})])` : value + },`; + } else { + const value = stringifyTypesAsIoTs(property.type); + interfacesOutput += `${JSON.stringify(property.name)}: ${ + item.fallback ? `t.union([${value}, t.array(${value})]),` : value + ',' + }`; + } + } + interfacesOutput += EOL + '})' + } + + if (item.extends.length > 0) { + interfacesOutput += '])' + } + + interfacesOutput += ';'; + } + + let declarationsOutput = ''; + let alreadyOutput: Record = {}; + for (const declaration of declarations.values()) { + declarationsOutput += EOL + EOL; + + const outputDeclaration = (declaration: IDeclaration) => { + if (declaration.name in alreadyOutput) { + return; + } + + for (const mightBeNeeded of declarations.values()) { + if (refersToName(declaration, mightBeNeeded)) { + outputDeclaration(mightBeNeeded); + } + } + + declarationsOutput += ' '; + + if (declaration.export) { + declarationsOutput += 'export '; + } + + declarationsOutput += `const ${declaration.name} = ${stringifyTypesAsIoTs(declaration.types)};` + EOL; + alreadyOutput[declaration.name] = true; + } + + outputDeclaration(declaration); + } + + for (const [k, v] of Object.entries(generics)) { + let type; + if (v.default_types) { + type = v.default_types; + } else { + type = 't.any'; + } + genericsOutput += `const ${k} = ${type};` + EOL; + } + + return header + EOL + genericsOutput + declarationsOutput + interfacesOutput; +} + +function refersToName(a: IDeclaration, b: IDeclaration) { + return a.types.some((type) => { + switch (type.type) { + case Type.String: + break; + case Type.Number: + break; + case Type.Length: + break; + case Type.StringLiteral: + break; + case Type.NumericLiteral: + break; + case Type.Alias: + if (type.name == b.name) { + return true; + } + } + return false; + }); +} + function stringifyTypes(types: DeclarableType | DeclarableType[]) { if (!Array.isArray(types)) { types = [types]; @@ -153,6 +269,37 @@ function stringifyTypes(types: DeclarableType | DeclarableType[]) { .join(' | '); } +function stringifyTypesAsIoTs(types: DeclarableType | DeclarableType[]) { + if (!Array.isArray(types)) { + return mapTypeToIoTS(types) + } + + if (types.length == 1) { + return mapTypeToIoTS(types[0]); + } + + return `t.union([ ${types + .map(mapTypeToIoTS) + .join(',')} ])`; + + function mapTypeToIoTS(type: DeclarableType) { + switch (type.type) { + case Type.String: + return 't.string'; + case Type.Number: + return 't.number'; + case Type.StringLiteral: + return `t.literal(${JSON.stringify(type.literal)})`; + case Type.NumericLiteral: + return 't.number'; + case Type.Alias: + return type.name; + case Type.Length: + return 'TLength'; + } + } +} + function stringifyGenerics(items: IGenerics[] | undefined, ignoreDefault = false) { if (!items || items.length === 0) { return ''; diff --git a/utils.ts b/utils.ts index 9387932..2a798b1 100644 --- a/utils.ts +++ b/utils.ts @@ -6,6 +6,7 @@ import { createInterface } from 'readline'; export const ROOT_DIR = __dirname; export const TYPESCRIPT_FILENAME = 'index.d.ts'; export const FLOW_FILENAME = 'index.js.flow'; +export const IOTS_FILENAME = 'csstype.ts'; export function writeFileAsync(filename: string, content: string) { return new Promise((resolve, reject) => { diff --git a/yarn.lock b/yarn.lock index f6f85a7..fc29cee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -340,13 +340,6 @@ dependencies: "@babel/types" "^7.3.0" -"@types/chokidar@^2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@types/chokidar/-/chokidar-2.1.3.tgz#123ab795dba6d89be04bf076e6aecaf8620db674" - integrity sha512-6qK3xoLLAhQVTucQGHTySwOVA1crHRXnJeLwqK6KIFkkKa2aoMFXh+WEi8PotxDtvN6MQJLyYN9ag9P6NLV81w== - dependencies: - chokidar "*" - "@types/concat-stream@^1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@types/concat-stream/-/concat-stream-1.6.0.tgz#394dbe0bb5fee46b38d896735e8b68ef2390d00d" @@ -821,21 +814,6 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chokidar@*: - version "3.2.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.2.1.tgz#4634772a1924512d990d4505957bf3a510611387" - integrity sha512-/j5PPkb5Feyps9e+jo07jUZGvkB5Aj953NrI4s8xSVScrAo/RHeILrtdb4uzR7N6aaFFxxJ+gt8mA8HfNpw76w== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.1.3" - optionalDependencies: - fsevents "~2.1.0" - chokidar@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.2.3.tgz#b9270a565d14f02f6bfdd537a6a2bbf5549b8c8c" @@ -1400,6 +1378,11 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +fp-ts@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.7.1.tgz#7bd754f3362f4b9bc4d272a97c0658e9a1bc16bc" + integrity sha512-rYy41jF1gVhBNYbPwup50dtyT686OKOoa86PXwh8aKpBRfmvPhnBh2zUkOYj84GIMSCsgY+oJ/RVhVKRvWNPTA== + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -1427,11 +1410,6 @@ fsevents@^1.2.7: nan "^2.12.1" node-pre-gyp "^0.12.0" -fsevents@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.0.tgz#ce1a5f9ac71c6d75278b0c5bd236d7dfece4cbaa" - integrity sha512-+iXhW3LuDQsno8dOIrCIT/CBjeBWuP7PXe8w9shnj9Lebny/Gx1ZjVBYwexLz36Ri2jKuXMNpV6CYNh8lHHgrQ== - fsevents@~2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.1.tgz#74c64e21df71721845d0c44fe54b7f56b82995a9" @@ -1686,6 +1664,11 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +io-ts@^2.2.9: + version "2.2.9" + resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.2.9.tgz#9a427512926462f20415099917a31dbf409ff2b0" + integrity sha512-Q9ob1VnpwyNoMam/BO6hm2dF4uu+to8NWSZNsRW6Q2Ni38PadgLZSQDo0hW7CJFgpJkQw4BXGwXzjr7c47c+fw== + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -3152,13 +3135,6 @@ readable-stream@^2.0.6, readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readdirp@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.1.3.tgz#d6e011ed5b9240a92f08651eeb40f7942ceb6cc1" - integrity sha512-ZOsfTGkjO2kqeR5Mzr5RYDbTGYneSkdNKX2fOX2P5jF7vMrd/GNnIAUtDldeHHumHUCQ3V05YfWUdxMPAsRu9Q== - dependencies: - picomatch "^2.0.4" - readdirp@~3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839"