-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor runtime type definition generator using AST modifications (#73)
- Loading branch information
1 parent
53d33de
commit 38a4c4e
Showing
4 changed files
with
197 additions
and
91 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'nxjs-runtime': patch | ||
--- | ||
|
||
Refactor runtime type definition generator using AST modifications |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,113 +1,214 @@ | ||
import fs from 'fs'; | ||
import ts from 'typescript'; | ||
import { generateDtsBundle } from 'dts-bundle-generator'; | ||
|
||
const distDir = new URL('dist/', import.meta.url); | ||
|
||
fs.mkdirSync(distDir, { recursive: true }); | ||
|
||
let [output] = generateDtsBundle( | ||
let [globalTypes, switchTypes, wasmTypes] = generateDtsBundle( | ||
[ | ||
{ | ||
filePath: './src/index.ts', | ||
}, | ||
{ | ||
filePath: './src/switch.ts', | ||
}, | ||
{ | ||
filePath: './src/wasm.ts', | ||
}, | ||
], | ||
{ | ||
noBanner: true, | ||
preferredConfigPath: 'tsconfig.json', | ||
} | ||
); | ||
|
||
// We need to do some post-processing on the `dts-bundle-generator` | ||
// output to make the result suitable as a runtime `.d.ts` file. | ||
|
||
// 1) Add references to core JavaScript interfaces | ||
// that are compatible with QuickJS. | ||
output = ` | ||
/// <reference no-default-lib="true"/> | ||
/// <reference lib="es2022" /> | ||
const globalNames = new Set(); | ||
|
||
${output}`; | ||
|
||
// 2) Remove all `export` declarations. | ||
output = output.replace(/^export /gm, ''); | ||
|
||
function splitOnComma(str) { | ||
let result = []; | ||
let depth = 0; | ||
let start = 0; | ||
|
||
for (let i = 0; i < str.length; i++) { | ||
switch (str[i]) { | ||
case '<': | ||
depth++; | ||
break; | ||
case '>': | ||
depth--; | ||
break; | ||
case ',': | ||
if (depth === 0) { | ||
result.push(str.slice(start, i).trim()); | ||
start = i + 1; | ||
} | ||
break; | ||
} | ||
} | ||
|
||
result.push(str.slice(start).trim()); // Add the last segment | ||
|
||
return result; | ||
} | ||
function transform(name, input, opts = {}) { | ||
// Remove banner | ||
input = input.split('\n').slice(2).join('\n'); | ||
|
||
// 3) Remove all `implements globalThis.` statements. | ||
output = output.replace(/\bimplements (.*){/g, (_, matches) => { | ||
const filtered = splitOnComma(matches).filter( | ||
(i) => !i.includes('globalThis.') | ||
const sourceFile = ts.createSourceFile( | ||
'file.d.ts', | ||
input, | ||
ts.ScriptTarget.Latest, | ||
true, | ||
ts.ScriptKind.TS | ||
); | ||
if (filtered.length > 0) { | ||
return `implements ${filtered.join(', ')} {`; | ||
|
||
function filterImplements(type) { | ||
return ( | ||
!type.expression.getText(sourceFile).startsWith(`${name}.`) && | ||
!type.typeArguments?.some((typeArgument) => | ||
typeArgument.getText(sourceFile).startsWith(`${name}.`) | ||
) | ||
); | ||
} | ||
return '{'; | ||
}); | ||
|
||
// 4) `ctx.canvas` is marked as `HTMLCanvasElement` to make TypeScript | ||
// happy, but the class name in nx.js is `Canvas`. So let's fix that. | ||
output = output.replace(/\bHTMLCanvasElement\b/g, 'Canvas'); | ||
|
||
// 5) At this point, the final line contains a `{};` which doesn't | ||
// hurt, but also isn't necessary. Clean that up. | ||
output = output.trim().split('\n').slice(0, -1).join('\n'); | ||
|
||
function generateNamespace(name, filePath) { | ||
let [output] = generateDtsBundle( | ||
[ | ||
{ | ||
filePath, | ||
}, | ||
], | ||
{ | ||
noBanner: true, | ||
|
||
function visit(node, context) { | ||
// For class/interface/type, remove if part of a namespace | ||
// where the type has already been defined in the global scope | ||
if ( | ||
ts.isClassDeclaration(node) || | ||
ts.isInterfaceDeclaration(node) || | ||
ts.isTypeAliasDeclaration(node) | ||
) { | ||
const name = node.name?.getText(); | ||
if (name) { | ||
if (opts.removeTypes) { | ||
if (opts.removeTypes.has(name)) return undefined; | ||
} else { | ||
globalNames.add(name); | ||
} | ||
} | ||
} | ||
); | ||
output = output.replace(/^export (declare )?/gm, ''); | ||
output = output.replace(/\bimplements (.*){/g, (_, matches) => { | ||
const filtered = matches | ||
.split(',') | ||
.map((i) => i.trim()) | ||
.filter((i) => !i.startsWith(`${name}.`)); | ||
if (filtered.length > 0) { | ||
return `implements ${filtered.join(', ')} {`; | ||
|
||
// Check for and remove stray 'export {}' | ||
if (ts.isExportDeclaration(node)) { | ||
if ( | ||
!node.exportClause || | ||
(ts.isNamedExports(node.exportClause) && | ||
node.exportClause.elements.length === 0) | ||
) { | ||
return undefined; // Remove this node | ||
} | ||
} | ||
|
||
if (ts.isClassDeclaration(node)) { | ||
// Filter or remove 'implements ${name}.*' from heritageClauses | ||
const newHeritageClauses = node.heritageClauses | ||
?.filter((clause) => { | ||
if (clause.token === ts.SyntaxKind.ImplementsKeyword) { | ||
return clause.types.filter(filterImplements).length > 0; // Keep the clause only if there are types left after filtering | ||
} | ||
return true; // Keep 'extends' and other clauses | ||
}) | ||
.map((clause) => { | ||
// Update the clause with the filtered types | ||
if (clause.token === ts.SyntaxKind.ImplementsKeyword) { | ||
return ts.factory.updateHeritageClause( | ||
clause, | ||
clause.types.filter(filterImplements) | ||
); | ||
} | ||
return clause; | ||
}); | ||
|
||
node = ts.factory.updateClassDeclaration( | ||
node, | ||
node.modifiers, | ||
node.name, | ||
node.typeParameters, | ||
newHeritageClauses?.length > 0 ? newHeritageClauses : undefined, | ||
node.members | ||
); | ||
} | ||
return '{'; | ||
}); | ||
output = output.trim().split('\n').slice(2, -2).map(l => ` ${l}`).join('\n'); | ||
//console.log({ output }); | ||
return `declare namespace ${name} {\n${output}\n}\n`; | ||
|
||
// Process class, interface, type alias, and function declarations | ||
if ( | ||
(ts.isClassDeclaration(node) || | ||
ts.isInterfaceDeclaration(node) || | ||
ts.isTypeAliasDeclaration(node) || | ||
ts.isFunctionDeclaration(node) || | ||
ts.isVariableStatement(node)) && | ||
node.modifiers?.some( | ||
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword | ||
) | ||
) { | ||
// Remove the 'export' modifier | ||
const newModifiers = node.modifiers.filter( | ||
(modifier) => modifier.kind !== ts.SyntaxKind.ExportKeyword | ||
); | ||
|
||
if (ts.isClassDeclaration(node)) { | ||
return ts.factory.updateClassDeclaration( | ||
node, | ||
newModifiers, | ||
node.name, | ||
node.typeParameters, | ||
node.heritageClauses, | ||
node.members | ||
); | ||
} else if (ts.isInterfaceDeclaration(node)) { | ||
return ts.factory.updateInterfaceDeclaration( | ||
node, | ||
newModifiers, | ||
node.name, | ||
node.typeParameters, | ||
node.heritageClauses, | ||
node.members | ||
); | ||
} else if (ts.isTypeAliasDeclaration(node)) { | ||
return ts.factory.updateTypeAliasDeclaration( | ||
node, | ||
newModifiers, | ||
node.name, | ||
node.typeParameters, | ||
node.type | ||
); | ||
} else if (ts.isFunctionDeclaration(node)) { | ||
return ts.factory.updateFunctionDeclaration( | ||
node, | ||
newModifiers, | ||
node.asteriskToken, | ||
node.name, | ||
node.typeParameters, | ||
node.parameters, | ||
node.type, | ||
node.body | ||
); | ||
} else if (ts.isVariableStatement(node)) { | ||
return ts.factory.updateVariableStatement( | ||
node, | ||
newModifiers, | ||
node.declarationList | ||
); | ||
} | ||
} | ||
|
||
return ts.visitEachChild( | ||
node, | ||
(child) => visit(child, context), | ||
context | ||
); | ||
} | ||
|
||
const result = ts.transform(sourceFile, [ | ||
(context) => (rootNode) => | ||
ts.visitNode(rootNode, (node) => visit(node, context)), | ||
]); | ||
return ts.createPrinter().printFile(result.transformed[0]).trim(); | ||
} | ||
|
||
function namespace(name, input) { | ||
return `declare namespace ${name} { | ||
${transform(name, input, { removeTypes: globalNames }) | ||
.split('\n') | ||
.map((l) => `\t${l}`) | ||
.join('\n')} | ||
}`; | ||
} | ||
|
||
// `dts-bundle-generator` does not currently support namespaces, | ||
// so we need to add those in manually: | ||
// See: https://github.com/timocov/dts-bundle-generator/issues/134 | ||
output += generateNamespace('WebAssembly', './src/wasm.ts'); | ||
output += generateNamespace('Switch', './src/switch.ts'); | ||
const output = `/// <reference no-default-lib="true"/> | ||
/// <reference lib="es2022" /> | ||
${transform('globalThis', globalTypes)} | ||
/** | ||
* The \`Switch\` global object contains native interfaces to interact with the Switch hardware. | ||
*/ | ||
${namespace('Switch', switchTypes)} | ||
/** | ||
* The \`WebAssembly\` JavaScript object acts as the namespace for all | ||
* {@link https://developer.mozilla.org/docs/WebAssembly | WebAssembly}-related functionality. | ||
* | ||
* Unlike most other global objects, \`WebAssembly\` is not a constructor (it is not a function object). | ||
* | ||
* @see https://developer.mozilla.org/docs/WebAssembly | ||
*/ | ||
${namespace('WebAssembly', wasmTypes)} | ||
`; | ||
|
||
fs.writeFileSync(new URL('index.d.ts', distDir), output); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters