diff --git a/.changeset/wise-masks-sing.md b/.changeset/wise-masks-sing.md
new file mode 100644
index 00000000..29a16cd6
--- /dev/null
+++ b/.changeset/wise-masks-sing.md
@@ -0,0 +1,5 @@
+---
+'nxjs-runtime': patch
+---
+
+Refactor runtime type definition generator using AST modifications
diff --git a/package.json b/package.json
index 2d17ce11..5451d504 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"scripts": {
"build": "turbo run build",
"bundle": "turbo run bundle",
- "format": "prettier --write \"**/*.{ts,js}\"",
+ "format": "prettier --write \"**/*.{ts,js,mjs}\"",
"ci:version": "changeset version && node .github/scripts/cleanup-examples.mjs && pnpm install --no-frozen-lockfile",
"ci:publish": "pnpm publish -r && node .github/scripts/create-git-tag.mjs"
},
diff --git a/packages/runtime/build.mjs b/packages/runtime/build.mjs
index 186589b3..9be59328 100644
--- a/packages/runtime/build.mjs
+++ b/packages/runtime/build.mjs
@@ -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 = `
-///
-
-///
+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 = `///
+///
+
+${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);
diff --git a/update-apps.mjs b/update-apps.mjs
index 5a2e6e8a..b3063a8c 100644
--- a/update-apps.mjs
+++ b/update-apps.mjs
@@ -3,7 +3,7 @@ import fs from 'fs';
const appsDir = new URL('apps/', import.meta.url);
for (const app of fs.readdirSync(appsDir)) {
- if (app === '.DS_Store') continue;
- const appUrl = new URL(`${app}/`, appsDir);
- // … do your thing…
+ if (app === '.DS_Store') continue;
+ const appUrl = new URL(`${app}/`, appsDir);
+ // … do your thing…
}