4

Question:

Regarding compiling TypeScript code server-side, is there a way to get a list of all the reference paths either for a single .ts file - or better, the whole compilation (starting from a single .ts file)? In order, preferably.

I'd prefer to use the existing parser if possible, rather than parsing the files with new code.

Context:

Since I don't think it exactly exists, I want to write:

  1. a server-side web user control that takes a .ts path and generates a cache-busting script tag pointing to
  2. an HttpHandler that compiles the requested .ts file ONCE at first request and then adds CacheDependencies to all the reference dependencies paths. When a file changes, the script-generating web user control updates it's cache-busting suffix of subsequent requests.

so in Release Mode, <tsb:typescript root="app.ts" runat="server" /> yields

<script type="text/javascript" src="app.ts?32490839"></script>

where the delivered script is the on-demand, cached single-file script.

And in Debug Mode the unmodified tag instead yields:

<script type="text/javascript" src="dependency1.ts?32490839"></script>
<script type="text/javascript" src="dependency2.ts?32490839"></script>
<script type="text/javascript" src="app.ts?32490839"></script>

As far as I've looked, this mode of operation is not supported by the TypeScript Visual Studio plugin nor any of the Optimizer bundlers. The bundlers do close to what I'm asking for, but they don't cache-bust and they don't single-file compile without annoying explicit bundling of the files.

I don't mind any performance hit at the very first request while the scripts are compiled. Besides that, perhaps there's a really great reason that this setup shouldn't or can't exist. If this cannot or clearly should not be done, I'd appreciate answers in that vein as well.

I've seen other questions on StackOverflow that dance around this desire in my interpretation, but nothing so explicit as this and none with relevant answers.

Thanks!

Also, is executing tsc.exe in a different process the best way for my HttpHandler to compile at runtime or is there a slick, safe, and simple way to do this in-process?

Jason Kleban
  • 20,024
  • 18
  • 75
  • 125

3 Answers3

4

In 2021 we have --explainFiles

If you want to examine your codebase more carefully (such as differentiating between type-only imports and runtime imports), you can use the Typescript API. The possibilities are infinite, but perhaps these parts below help set you in a good direction (probably has bugs):

import * as ts from "typescript";

interface FoundReference {
    typeOnly: boolean;
    relativePathReference: boolean;
    referencingPath: string;
    referencedSpecifier: string;
}

const specifierRelativeFile = /^\..*(?<!\.(less|svg|png|woff))$/;
const specifierNodeModule = /^[^\.]/;

const diveDeeper = (path: string, node: ts.Node, found: FoundReference[]) =>
    Promise.all(node.getChildren().map(n => findAllReferencesNode(path, n, found)));

const findAllReferencesNode = async (path: string, node: ts.Node, found: FoundReference[]) => {
    switch (node.kind) {
        case ts.SyntaxKind.ExportDeclaration:
            const exportDeclaration = node as ts.ExportDeclaration;

            if (exportDeclaration.moduleSpecifier) {
                const specifier = (exportDeclaration.moduleSpecifier as ts.StringLiteral).text;

                if (specifier) {
                    if (specifierRelativeFile.test(specifier)) {
                        found.push({
                            typeOnly: exportDeclaration.isTypeOnly,
                            relativePathReference: true,
                            referencingPath: path,
                            referencedSpecifier: specifier
                        });
                    } else if (specifierNodeModule.test(specifier)) {
                        found.push({
                            typeOnly: exportDeclaration.isTypeOnly,
                            relativePathReference: false,
                            referencingPath: path,
                            referencedSpecifier: specifier
                        });
                    }
                }
            }

            break;
        case ts.SyntaxKind.ImportDeclaration:
            const importDeclaration = node as ts.ImportDeclaration;
            const importClause = importDeclaration.importClause;

            const specifier = (importDeclaration.moduleSpecifier as ts.StringLiteral).text;

            if (specifier) {
                if (specifierRelativeFile.test(specifier)) {
                    found.push({
                        typeOnly: (!!importClause && !importClause.isTypeOnly),
                        relativePathReference: true,
                        referencingPath: path,
                        referencedSpecifier: specifier
                    });
                } else if (specifierNodeModule.test(specifier)) {
                    found.push({
                        typeOnly: (!!importClause && !importClause.isTypeOnly),
                        relativePathReference: false,
                        referencingPath: path,
                        referencedSpecifier: specifier
                    });
                }
            }

            break;
        case ts.SyntaxKind.CallExpression:
            const callExpression = node as ts.CallExpression;

            if ((callExpression.expression.kind === ts.SyntaxKind.ImportKeyword ||
                (callExpression.expression.kind === ts.SyntaxKind.Identifier &&
                    callExpression.expression.getText() === "require")) &&
                callExpression.arguments[0]?.kind === ts.SyntaxKind.StringLiteral) {

                const specifier = (callExpression.arguments[0] as ts.StringLiteral).text;

                if (specifierRelativeFile.test(specifier)) {
                    found.push({
                        typeOnly: false,
                        relativePathReference: true,
                        referencingPath: path,
                        referencedSpecifier: specifier
                    });
                } else if (specifierNodeModule.test(specifier)) {
                    found.push({
                        typeOnly: false,
                        relativePathReference: false,
                        referencingPath: path,
                        referencedSpecifier: specifier
                    });
                } else {
                    await diveDeeper(path, node, found);
                }
            } else {
                await diveDeeper(path, node, found);
            }

            break;
        default:
            await diveDeeper(path, node, found);

            break;
    }
}

const path = "example.ts";

const source = `
import foo from "./foo";
import * as bar from "./bar";
import { buzz } from "./fizz/buzz";

export foo from "./foo";
export * as bar from "./bar";
export { buzz } from "./fizz/buzz";

const whatever = require("whatever");

const stuff = async () => {
    require("whatever");

    const x = await import("xyz");
}
`

const rootNode = ts.createSourceFile(
    path,
    source,
    ts.ScriptTarget.Latest,
    /*setParentNodes */ true
);

const found: FoundReference[] = [];

findAllReferencesNode(path, rootNode, found)
.then(() => { 
    console.log(found); 
});

[
  {
    "typeOnly": true,
    "relativePathReference": true,
    "referencingPath": "example.ts",
    "referencedSpecifier": "./foo"
  },
  {
    "typeOnly": true,
    "relativePathReference": true,
    "referencingPath": "example.ts",
    "referencedSpecifier": "./bar"
  },
  {
    "typeOnly": true,
    "relativePathReference": true,
    "referencingPath": "example.ts",
    "referencedSpecifier": "./fizz/buzz"
  },
  {
    "typeOnly": false,
    "relativePathReference": true,
    "referencingPath": "example.ts",
    "referencedSpecifier": "./bar"
  },
  {
    "typeOnly": false,
    "relativePathReference": true,
    "referencingPath": "example.ts",
    "referencedSpecifier": "./fizz/buzz"
  },
  {
    "typeOnly": false,
    "relativePathReference": false,
    "referencingPath": "example.ts",
    "referencedSpecifier": "whatever"
  },
  {
    "typeOnly": false,
    "relativePathReference": false,
    "referencingPath": "example.ts",
    "referencedSpecifier": "whatever"
  },
  {
    "typeOnly": false,
    "relativePathReference": false,
    "referencingPath": "example.ts",
    "referencedSpecifier": "xyz"
  }
] 

Once you have the referencedSpecifier, you need some basic logic to resolve it to the next path and repeat your exploration with that next resolved file, resursively.

Jason Kleban
  • 20,024
  • 18
  • 75
  • 125
1

Bundles do this for JavaScript if you enable optimisations and use the bundle HTML helper too add the bundle to the page. The cache-bust URL is a short hash of the JavaScript file, so if you change any script in the bundle, the new hash breaks it out of the client cache.

My recommendation is to bundle the compiled JavaScript and have the bundler minify and cache-bust. You can get TypeScript to generate a single file with all dependencies using the out flag...

tsc --out final.js app.ts

Your bundle now only needs to include final.js - which saves on explicitly listing all files and adding new ones later.

You can still write something to intercept and compile at runtime. My preference is to have this done before runtime as one of the benefits of TypeScript is compile-time checking - how will you handle a compile error if the script is being demanded by a client. Even if you do it at runtime, I would still add the JavaScript reference to the page, not the .ts file.

Fenton
  • 241,084
  • 71
  • 387
  • 401
  • Yes, I want to run this tsc command (or an in-process equivelent), but I want to be able to run it when any of the files change. So, in debug mode, as I'm running the application but making small changes and reloading the page, it busts the cache and recompiles. Without a list of dependencies of the single-file output I can't do that. Compiling ahead-of-time, even as an automated build step, is not enough because the changes can be more frequent than between Visual Studio Project builds. Do you understand the difference in the scenario that I'm describing from what `tsc --out` can provide? – Jason Kleban Feb 03 '13 at 14:04
  • I'm aware of a `--watch` option of tsc, but while it's similar it's node.js requirement and independent process is bulky. – Jason Kleban Feb 03 '13 at 14:06
  • With the 0.8.2.0 option to compile on save, you would instantly have the JavaScript output for any file you edit. – Fenton Feb 03 '13 at 14:38
  • Yes, but it isn't single-file compilation. So I'm trying to get both features and without explicit listing of scripts in bundles or on page. And browser cache management. – Jason Kleban Feb 04 '13 at 12:51
  • It makes no sense to me to compile a single file if that file has dependencies. How do you know if the usage of the dependency is valid without including it in compilation. – Fenton Feb 04 '13 at 14:11
  • I do want to include it in the compilation. Not sure what you mean. In my example, `page.html` references `app.ts` and `app.ts` references `dependency1.ts` and `dependency2.ts`. After getting `page` we need to have `app.ts` compiled as a single file, as `--out` does. But I don't want the script to get stale if I update any of the ts files nor do I want to have to manually recompile with `--out`. And I don't want to have to manage a bundle of scripts because that is redundant - app.ts already points to it's dependencies and I shouldn't have to maintain the list again elsewhere. – Jason Kleban Feb 04 '13 at 14:44
  • I have the same issue. I have a "project" file for my typescript app that has references to all of my source files. I can't find a good solution for this. Did you find anything? – Eric J. Smith Apr 05 '13 at 16:34
1

You can use Madge, either interactively or in node.

See also: How can I see the full nodejs "require()" tree starting at a given file?

Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689