diff --git a/package.json5 b/package.json5 index 5ede49d..921d164 100644 --- a/package.json5 +++ b/package.json5 @@ -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', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2eb420d..cee7e06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/Complex/type.ts b/src/Complex/type.ts index 44525d6..ff42a3b 100644 --- a/src/Complex/type.ts +++ b/src/Complex/type.ts @@ -17,7 +17,8 @@ export const Complex_type = { (z: Complex) => joinTypes(dep.typeOf(z.re), dep.typeOf(z.im)), from: { Complex: (dep: {convert: CommonSignature['convert']}) => - (z: Complex) => ({re: dep.convert(z.re), im: dep.convert(z.im)}), + (z: Complex): Complex => + ({re: dep.convert(z.re), im: dep.convert(z.im)}), T: (dep: {zero: CommonSignature['zero']}) => (t: T) => ({re: t, im: dep.zero(t)}) } diff --git a/src/core/Dispatcher.ts b/src/core/Dispatcher.ts index 69b90ee..198b69a 100644 --- a/src/core/Dispatcher.ts +++ b/src/core/Dispatcher.ts @@ -102,6 +102,10 @@ type Implementations< Specs extends Specifications > = {[K in NeedKeys]: ImpType} +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} > - ship(): Implementations + ship(info?: ReflectedTypeInfo): Implementations & ReflectedTypeInfo } // And a function that actually provides the builder interface: @@ -187,9 +191,8 @@ function impBuilder< Specs & {[K in NewKeys]: DepCheck} > }, - ship() { - return (sofar as - Implementations) + ship(info?: ReflectedTypeInfo) { + return { ...sofar, ...info } } } } diff --git a/src/index.ts b/src/index.ts index 66d4a5b..c575454 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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})) diff --git a/tools/reflectTypes.mjs b/tools/reflectTypes.mjs new file mode 100644 index 0000000..c232072 --- /dev/null +++ b/tools/reflectTypes.mjs @@ -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) + } + } +} diff --git a/tools/ts2json.mjs b/tools/ts2json.mjs new file mode 100644 index 0000000..4013acd --- /dev/null +++ b/tools/ts2json.mjs @@ -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] +}