feat: Try two different kinds of type reflection (#5)

Co-authored-by: Jos de Jong <wjosdejong@gmail.com>
Reviewed-on: #5
Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
Glen Whitney 2024-10-21 17:00:58 +00:00 committed by Glen Whitney
parent 239215c234
commit 59c325ff6c
7 changed files with 618 additions and 14 deletions

View File

@ -3,8 +3,9 @@
version: '0.0.1',
description: 'Another prototype for a math core',
scripts: {
build: 'tsc && cp etc/package.json build/',
go: 'pnpm build && pnpm start',
build: 'tsc && cpy etc/package.json build/ --flat',
reflect: 'node tools/reflectTypes.mjs',
go: 'pnpm build && pnpm reflect && pnpm start',
start: 'node --experimental-loader tsc-module-loader build',
test: 'echo Error no test specified && exit 1',
},
@ -21,9 +22,10 @@
url: 'https://code.studioinfinity.org/glen/math5.git',
},
devDependencies: {
'@types/node': '^22.7.5',
typescript: '^5.6.3',
'undici-types': '^6.20.0',
'@types/node': '22.7.5',
'cpy-cli': '5.0.0',
typescript: '5.6.3',
'undici-types': '6.20.0',
},
dependencies: {
'tsc-module-loader': '^0.0.1',

View File

@ -13,45 +13,204 @@ importers:
version: 0.0.1
devDependencies:
'@types/node':
specifier: ^22.7.5
specifier: 22.7.5
version: 22.7.5
cpy-cli:
specifier: 5.0.0
version: 5.0.0
typescript:
specifier: ^5.6.3
specifier: 5.6.3
version: 5.6.3
undici-types:
specifier: ^6.20.0
specifier: 6.20.0
version: 6.20.0
packages:
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
'@nodelib/fs.stat@2.0.5':
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
engines: {node: '>= 8'}
'@nodelib/fs.walk@1.2.8':
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@types/node@22.7.5':
resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==}
aggregate-error@4.0.1:
resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==}
engines: {node: '>=12'}
arrify@3.0.0:
resolution: {integrity: sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==}
engines: {node: '>=12'}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
clean-stack@4.2.0:
resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==}
engines: {node: '>=12'}
commonjs-extension-resolution-loader@0.1.0:
resolution: {integrity: sha512-XDCkM/cYIt1CfPs+LNX8nC2KKrzTx5AAlGLpx7A4BjWQCHR9LphDu9Iq5zXYf+PXhCkpLGBFiyiTnwmSnNxbWQ==}
cp-file@10.0.0:
resolution: {integrity: sha512-vy2Vi1r2epK5WqxOLnskeKeZkdZvTKfFZQCplE3XWsP+SUJyd5XAUFC9lFgTjjXJF2GMne/UML14iEmkAaDfFg==}
engines: {node: '>=14.16'}
cpy-cli@5.0.0:
resolution: {integrity: sha512-fb+DZYbL9KHc0BC4NYqGRrDIJZPXUmjjtqdw4XRRg8iV8dIfghUX/WiL+q4/B/KFTy3sK6jsbUhBaz0/Hxg7IQ==}
engines: {node: '>=16'}
hasBin: true
cpy@10.1.0:
resolution: {integrity: sha512-VC2Gs20JcTyeQob6UViBLnyP0bYHkBh6EiKzot9vi2DmeGlFT9Wd7VG3NBrkNx/jYvFBeyDOMMHdHQhbtKLgHQ==}
engines: {node: '>=16'}
dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
escape-string-regexp@5.0.0:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
globby@13.2.2:
resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
indent-string@5.0.0:
resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==}
engines: {node: '>=12'}
is-core-module@2.15.1:
resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
engines: {node: '>= 0.4'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
junk@4.0.1:
resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==}
engines: {node: '>=12.20'}
meow@12.1.1:
resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==}
engines: {node: '>=16.10'}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
nested-error-stacks@2.1.1:
resolution: {integrity: sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==}
p-event@5.0.1:
resolution: {integrity: sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
p-filter@3.0.0:
resolution: {integrity: sha512-QtoWLjXAW++uTX67HZQz1dbTpqBfiidsB6VtQUC9iR85S120+s0T5sO6s+B5MLzFcZkrEd/DGMmCjR+f2Qpxwg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
p-map@5.5.0:
resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==}
engines: {node: '>=12'}
p-map@6.0.0:
resolution: {integrity: sha512-T8BatKGY+k5rU+Q/GTYgrEf2r4xRMevAN5mtXc2aPc4rS1j3s+vWTaO2Wag94neXuCAUAs8cxBL9EeB5EA6diw==}
engines: {node: '>=16'}
p-timeout@5.1.0:
resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==}
engines: {node: '>=12'}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true
reusify@1.0.4:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
slash@4.0.0:
resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==}
engines: {node: '>=12'}
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
tsc-module-loader@0.0.1:
resolution: {integrity: sha512-3SIydFXw96jYU2imgULgIHKlUY8FnfDZlazvNmw4Umx/8qCwXsyDg0V2QOULf2Fw7zaI1Hbibh0mB8VzRZ/Ghg==}
@ -68,34 +227,178 @@ packages:
snapshots:
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
run-parallel: 1.2.0
'@nodelib/fs.stat@2.0.5': {}
'@nodelib/fs.walk@1.2.8':
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
'@types/node@22.7.5':
dependencies:
undici-types: 6.19.8
aggregate-error@4.0.1:
dependencies:
clean-stack: 4.2.0
indent-string: 5.0.0
arrify@3.0.0: {}
braces@3.0.3:
dependencies:
fill-range: 7.1.1
clean-stack@4.2.0:
dependencies:
escape-string-regexp: 5.0.0
commonjs-extension-resolution-loader@0.1.0:
dependencies:
resolve: 1.22.8
cp-file@10.0.0:
dependencies:
graceful-fs: 4.2.11
nested-error-stacks: 2.1.1
p-event: 5.0.1
cpy-cli@5.0.0:
dependencies:
cpy: 10.1.0
meow: 12.1.1
cpy@10.1.0:
dependencies:
arrify: 3.0.0
cp-file: 10.0.0
globby: 13.2.2
junk: 4.0.1
micromatch: 4.0.8
nested-error-stacks: 2.1.1
p-filter: 3.0.0
p-map: 6.0.0
dir-glob@3.0.1:
dependencies:
path-type: 4.0.0
escape-string-regexp@5.0.0: {}
fast-glob@3.3.2:
dependencies:
'@nodelib/fs.stat': 2.0.5
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.8
fastq@1.17.1:
dependencies:
reusify: 1.0.4
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
function-bind@1.1.2: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
globby@13.2.2:
dependencies:
dir-glob: 3.0.1
fast-glob: 3.3.2
ignore: 5.3.2
merge2: 1.4.1
slash: 4.0.0
graceful-fs@4.2.11: {}
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
ignore@5.3.2: {}
indent-string@5.0.0: {}
is-core-module@2.15.1:
dependencies:
hasown: 2.0.2
is-extglob@2.1.1: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-number@7.0.0: {}
junk@4.0.1: {}
meow@12.1.1: {}
merge2@1.4.1: {}
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
nested-error-stacks@2.1.1: {}
p-event@5.0.1:
dependencies:
p-timeout: 5.1.0
p-filter@3.0.0:
dependencies:
p-map: 5.5.0
p-map@5.5.0:
dependencies:
aggregate-error: 4.0.1
p-map@6.0.0: {}
p-timeout@5.1.0: {}
path-parse@1.0.7: {}
path-type@4.0.0: {}
picomatch@2.3.1: {}
queue-microtask@1.2.3: {}
resolve@1.22.8:
dependencies:
is-core-module: 2.15.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
reusify@1.0.4: {}
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
slash@4.0.0: {}
supports-preserve-symlinks-flag@1.0.0: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
tsc-module-loader@0.0.1:
dependencies:
commonjs-extension-resolution-loader: 0.1.0

View File

@ -17,7 +17,8 @@ export const Complex_type = {
(z: Complex<unknown>) => joinTypes(dep.typeOf(z.re), dep.typeOf(z.im)),
from: {
Complex: <U,T>(dep: {convert: CommonSignature<U,T>['convert']}) =>
(z: Complex<U>) => ({re: dep.convert(z.re), im: dep.convert(z.im)}),
(z: Complex<U>): Complex<T> =>
({re: dep.convert(z.re), im: dep.convert(z.im)}),
T: <T>(dep: {zero: CommonSignature<T>['zero']}) =>
(t: T) => ({re: t, im: dep.zero(t)})
}

View File

@ -102,6 +102,10 @@ type Implementations<
Specs extends Specifications<Signatures, NeedKeys, NeedList>
> = {[K in NeedKeys]: ImpType<Signatures, K, Specs[K]>}
export interface ReflectedTypeInfo {
reflectedType5: string
}
// The builder interface that lets us assemble narrowly-typed Implementations:
interface ImplementationBuilder<
Signatures extends GenSigs,
@ -135,7 +139,7 @@ interface ImplementationBuilder<
Specs & {[K in NewKeys]: DepCheck<RD, Signatures>}
>
ship(): Implementations<Signatures, NeedKeys, NeedList, Specs>
ship(info?: ReflectedTypeInfo): Implementations<Signatures, NeedKeys, NeedList, Specs> & ReflectedTypeInfo
}
// And a function that actually provides the builder interface:
@ -187,9 +191,8 @@ function impBuilder<
Specs & {[K in NewKeys]: DepCheck<RD, Signatures, DepKeys>}
>
},
ship() {
return (sofar as
Implementations<Signatures, NeedKeys, NeedList, Specs>)
ship(info?: ReflectedTypeInfo) {
return { ...sofar, ...info }
}
}
}

View File

@ -2,4 +2,4 @@ import {inspect} from 'node:util'
import * as specifications from './all'
console.log(inspect(specifications, {depth: 8, colors: true}))
console.log(inspect(specifications, {depth: 18, colors: true}))

62
tools/reflectTypes.mjs Normal file
View File

@ -0,0 +1,62 @@
import { readFileSync, writeFileSync, readdirSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join, relative } from 'node:path'
import ts2json from './ts2json.mjs'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const buildDir = join(__dirname, '..', 'build')
const files = (await readdirSync(buildDir, { recursive: true }))
.filter(file => file.endsWith('.js'))
.map(file => join(buildDir, file))
for (const file of files) {
reflectType5(file, { debug: true })
}
function reflectType5(srcFile, options = { debug: false }) {
log(`=========Reflecting file "${relative(__dirname, srcFile)}"`)
const defFile = srcFile.replace(/.js$/, '.d.ts')
const src = String(readFileSync(srcFile))
const defs = String(readFileSync(defFile))
const parsedDefs = ts2json(defFile)
const typeDefMatches = defs.matchAll(/: ({(?:(?!\n}).)+\n}) & (?:(?!ReflectedTypeInfo).)+ReflectedTypeInfo/gs)
if (!typeDefMatches) {
log('No ReflectedTypeInfo found.')
return
}
const typeDefs = Array.from(typeDefMatches).map(def => def[1])
log(` ${typeDefs.length} ReflectedTypeInfo found`)
let index = 0
let srcReflected = src.replaceAll(/(\s*)\.ship\(\)/g, () => {
const def = typeDefs[index]
index++
return `.ship({ reflectedType5: \`${def}\` })`
})
log(` ReflectedTypeInfo injected in ${index} occurrences of .ship()`)
if (index !== typeDefs.length) {
log(' WARNING: not all ReflectedTypeInfo occurrences could be injected')
}
for (const id in parsedDefs) {
if (id.includes('interface')) continue
if (parsedDefs[id] === undefined) continue
log(` Tagging ${id} with type data`, parsedDefs[id])
srcReflected +=
`\n${id}._reflectedType5 = ${JSON.stringify(parsedDefs[id])}\n`
}
writeFileSync(srcFile, srcReflected)
function log(...args) {
if (options.debug) {
console.log(...args)
}
}
}

233
tools/ts2json.mjs Normal file
View File

@ -0,0 +1,233 @@
import * as ts from 'typescript'
const intrinsicTypeKeywords = new Set([
ts.SyntaxKind.AnyKeyword,
ts.SyntaxKind.BooleanKeyword,
ts.SyntaxKind.NeverKeyword,
ts.SyntaxKind.NumberKeyword,
ts.SyntaxKind.StringKeyword,
ts.SyntaxKind.UndefinedKeyword,
ts.SyntaxKind.UnknownKeyword,
ts.SyntaxKind.VoidKeyword
])
const typeOperatorKeywords = new Map([
[ts.SyntaxKind.KeyofKeyword, '_keyof'],
[ts.SyntaxKind.ReadonlyKeyword, '_readonly'],
[ts.SyntaxKind.UniqueKeyword, '_unique'],
])
class TSNode {
constructor(name, type) {
this.children = []
this.addChild = (name, type) => {
let node = new TSNode(name, type)
this.children.push(node)
return node
}
this.getType = () => this.type
this.getObject = () => {
let map = {}
map[this.name] = this.children.length
? this.children
.map(child => child.getObject())
.reduce((pv, child) => {
for (let key in child) {
if (pv.hasOwnProperty(key) || key in pv) {
if (child[key]) {
Object.assign(pv[key], child[key])
}
} else {
pv[key] = child[key]
}
}
return pv
}, {})
: this.type
return map
};
this.name = name
this.type = type
}
}
function parameterTypeStructure(paramNode, checker) {
let typeStruc = typeStructure(paramNode.type, checker)
if (paramNode.questionToken) typeStruc = {_optional: typeStruc}
return typeStruc
}
function typeStructure(typeNode, checker) {
switch (typeNode.kind) {
case ts.SyntaxKind.UnionType:
return {
_union: typeNode.types.map(t => typeStructure(t, checker))
}
case ts.SyntaxKind.IntersectionType:
return {
_intersection: typeNode.types.map(t => typeStructure(t, checker))
}
case ts.SyntaxKind.ImportType: {
const typeStruc = {
_importedFrom: typeNode.argument.literal.text,
_name: typeNode.qualifier.text
}
if (typeNode.typeArguments) {
typeStruc._typeArguments = typeNode.typeArguments.map(
p => typeStructure(p, checker))
}
return typeStruc
}
case ts.SyntaxKind.ArrayType:
return {_array: typeStructure(typeNode.elementType, checker)}
case ts.SyntaxKind.TypeLiteral: // Seems to be plain object types
return Object.fromEntries(typeNode.members.map(
mem => [mem.name.text, typeStructure(mem.type, checker)]))
case ts.SyntaxKind.FunctionType: {
const typeStruc = {
_parameters: typeNode.parameters.map(
p => parameterTypeStructure(p, checker)),
_returns: typeStructure(typeNode.type, checker)
}
if (typeNode.typeParameters) {
typeStruc._typeParameters = typeNode.typeParameters.map(
p => p.name.text)
}
return typeStruc
}
case ts.SyntaxKind.IndexedAccessType:
return {
_ofType: typeStructure(typeNode.objectType, checker),
_index: typeStructure(typeNode.indexType, checker)
}
case ts.SyntaxKind.TypeOperator:
const key = typeOperatorKeywords.get(
typeNode.operator,
'_unidentified_operator'
)
return {
[key]:
typeStructure(typeNode.type, checker)
}
case ts.SyntaxKind.ConditionalType:
return {
_subtype: typeStructure(typeNode.checkType, checker),
_basetype: typeStructure(typeNode.extendsType, checker),
_truetype: typeStructure(typeNode.trueType, checker),
_falsetype: typeStructure(typeNode.falseType, checker),
}
case ts.SyntaxKind.TypeReference: {
const typeStruc = {_typeParameter: typeNode.typeName.text}
if (typeNode.typeArguments) {
typeStruc._typeArguments = typeNode.typeArguments.map(
arg => typeStructure(arg, checker))
}
return typeStruc
}
case ts.SyntaxKind.TypePredicate:
return {_is: typeStructure(typeNode.type, checker)}
case ts.SyntaxKind.LiteralType:
return checker.typeToString(checker.getTypeFromTypeNode(typeNode))
default:
if (intrinsicTypeKeywords.has(typeNode.kind)) {
return checker.getTypeFromTypeNode(typeNode).intrinsicName
}
}
throw new Error(`Unhandled type node ${ts.SyntaxKind[typeNode.kind]}`)
}
const visit = (parent, checker) => node => {
switch (node.kind) {
// Currently, we are ignoring the following sorts of statements
// that may appear in .d.ts files. We may need to revisit these,
// especially the InterfaceDeclaration and TypeAliasDeclaration,
// if we want to generate runtime information on pure type
// declarations. I think this may be necessary for example to compute
// the "RealType" of a type at runtime.
case ts.SyntaxKind.EndOfFileToken:
case ts.SyntaxKind.ExportDeclaration:
case ts.SyntaxKind.ImportDeclaration:
case ts.SyntaxKind.InterfaceDeclaration:
case ts.SyntaxKind.TypeAliasDeclaration:
break
case ts.SyntaxKind.VariableStatement:
node.declarationList.declarations.forEach(visit(parent, checker))
break
case ts.SyntaxKind.VariableDeclaration: {
const typeStruc = typeStructure(node.type, checker)
parent.addChild(node.name.text, typeStruc)
break
}
case ts.SyntaxKind.FunctionDeclaration: {
const typeStruc = {
_parameters: node.parameters.map(
p => parameterTypeStructure(p, checker)),
_returns: typeStructure(node.type, checker)
}
if (node.typeParameters) {
typeStruc._typeParameters = node.typeParameters.map(
p => p.name.text)
}
parent.addChild(node.name.text, typeStruc)
break
}
case ts.SyntaxKind.ModuleDeclaration:
let moduleName = node.name.text
visit(parent.addChild(moduleName), checker)(node.body)
break
case ts.SyntaxKind.ModuleBlock:
ts.forEachChild(node, visit(parent, checker));
break
case ts.SyntaxKind.PropertySignature:
let propertyName = node.name
let propertyType = node.type
let arrayDeep = 0
let realPropertyName =
'string' !== typeof propertyName && 'text' in propertyName
? propertyName.text
: propertyName
console.log('Property', realPropertyName)
while (propertyType.kind === ts.SyntaxKind.ArrayType) {
arrayDeep++
propertyType = propertyType.elementType
}
if (propertyType.kind === ts.SyntaxKind.TypeReference) {
let realPropertyType = propertyType.typeName
parent.addChild(
realPropertyName,
'Array<'.repeat(arrayDeep) +
(realPropertyType.kind === ts.SyntaxKind.QualifiedName
? realPropertyType.getText()
: 'text' in realPropertyType
? realPropertyType.text
: realPropertyType) +
'>'.repeat(arrayDeep)
)
} else {
if (intrinsicTypeKeywords.has(propertyType.kind)) {
parent.addChild(
realPropertyName,
checker.getTypeFromTypeNode(propertyType).intrinsicName)
}
}
break
default:
console.warn(
'Unhandled node kind',
node.kind, ts.SyntaxKind[node.kind])
}
}
export default function(filename, options = {}) {
const ROOT_NAME = 'root'
const node = new TSNode(ROOT_NAME)
let program = ts.createProgram([filename], options)
let checker = program.getTypeChecker()
const sourceFiles = program.getSourceFiles()
let sourceFile = sourceFiles.find(file => file.fileName === filename)
ts.forEachChild(sourceFile, visit(node, checker))
return node.getObject()[ROOT_NAME]
}