Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

In browser linting #1001

Closed
christianalfoni opened this issue Feb 28, 2016 · 10 comments
Closed

In browser linting #1001

christianalfoni opened this issue Feb 28, 2016 · 10 comments

Comments

@christianalfoni
Copy link

Hi there,

I was wondering if it is technically possible to do Typescript linting in the browser... well, everything is possible, but is it a huge undertaking? Projects like eslint and coffee-lint does this. The benefit is allowing in browser code editors to have linting. I am working on one of those services, http://webpackbin.herokuapp.com, but there is of course lots of other services that could benefit from this. Like JSBin, cloud9, nitrous etc.

So yeah, kind of an open question. Thanks!

@iulia-codes
Copy link

Hi, same question, is it possible at this time to do Typescript linting in the browser?

@ajafff
Copy link
Contributor

ajafff commented Sep 29, 2017

It's not possible. TSLint uses several node.js APIs (fs, path, require of node_modules like glob, and so on) to get the job done. In a browser environment, all those APIs need to be mocked.

A better way is introducing some kind of abstraction like TypeScript's CompilerHost. That's a rather big refactoring, though.

I'll take a look at eslint to see how they do it.

@ajafff
Copy link
Contributor

ajafff commented Sep 29, 2017

For posterity: here's the discussion in the eslint repo eslint/eslint#2585 and eslint/eslint#8348
They don't support other runtimes than node.

@aervin
Copy link
Contributor

aervin commented Dec 21, 2017

Was curious about what it would take to mimic the eslint demo feature and stumbled on this discussion. It looks like TSLint relies on those Node packages primarily for configuration handling, and the Linter itself does not make much use of them. Is that fair to say? I'm NOT suggesting TSLint support the browser, but how about recreating that feature for the docs? People seem to want code examples, and a TSLint Playground would address that in a big way.

@ajafff
Copy link
Contributor

ajafff commented Dec 21, 2017

@aervin I don't think this solves our documentation issue. But it would be nice to just test rules without installing everything locally.

Almost all rules don't do any IO, so they would work in the browser. But there are some parts that rely heavily on a node environment:

  • creating (and updating) the typescript project
    • requires a different implementation of CompilerHost to work
    • this is necessary for typed rules to work
  • globbing files to lint
    • probably not necessary in the browser
  • finding the nearest config and all configurations it extends
    • could be solved with a static config, that is written in the browser
  • (lazy) loading of rules
  • --fix would probably need to be disabled

@aervin
Copy link
Contributor

aervin commented Dec 21, 2017

Thanks @ajafff. Looks like a simple proof of concept is in order to gather requirements for the browser. And soon I'll post a proposal for adding code examples to the rule docs.

@aervin
Copy link
Contributor

aervin commented Dec 24, 2017

Update:
I was able to lint TS source in the browser using a combination of webpack, typescript, and tslint (a bastardized version of the Linter class, essentially). Here's a recording demonstrating a simple browser implementation. With a single rule loaded (no-console), the following works in Chrome dev console environment:

localStorage.setItem('source', 'console.log(error);');
document.linter.lint("TSLintPlayground.ts");

 /* logs RuleFailure[] w/ single element (no-console error) */
document.linter.failures

/* Do stuff with the result! */

CompilerHost

import * as ts from "typescript";

export default function createCompilerHostForBrowser(
    options: ts.CompilerOptions,
    moduleSearchLocations: string[],
): ts.CompilerHost {
    return {
        getSourceFile,
        getDefaultLibFileName: () => "lib.d.ts",
        writeFile: (fileName, content) =>
            localStorage.setItem("source", content),
        getCurrentDirectory: () => "/",
        getDirectories: path => ["/"],
        getCanonicalFileName: fileName => "TSLintPlaygroundFile.ts",
        getNewLine: () => "\n",
        useCaseSensitiveFileNames: function() {
            return false;
        },
        fileExists,
        readFile,
        resolveModuleNames,
    };

    function fileExists(fileName: string): boolean {
        return localStorage.getItem("source") !== undefined;
    }

    function readFile(fileName: string): string | undefined {
        return localStorage.getItem("source");
    }

    function getSourceFile(
        fileName: string,
        languageVersion: ts.ScriptTarget,
        onError?: (message: string) => void,
    ) {
        const sourceText = localStorage.getItem("source");
        return sourceText !== undefined
            ? ts.createSourceFile(
                  this.getCanonicalFileName(),
                  sourceText,
                  languageVersion,
              )
            : undefined;
    }

    function resolveModuleNames(
        moduleNames: string[],
        containingFile: string,
    ): ts.ResolvedModule[] {
        return [
            {
                resolvedFileName: "./mod.js", /* I'm not sure what's up with this */
            },
        ];
    }
}

Webpack

/* webpack.config.js */

module.exports = {
    entry: "./src/app.ts",
    output: {
        filename: "bundle.js",
        path: __dirname,
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                loader: "ts-loader",
                exclude: /node_modules/,
            },
        ],
    },
    resolve: {
        extensions: [".tsx", ".ts", ".js"],
    },
    node: {
        os: "empty",
        fs: "empty",
        glob: "empty"
    },
};

Significant changes to TSLint

The createProgram method can be simplified for the browser it seems

public static createProgram(): ts.Program {
    const options: ts.CompilerOptions = {
        module: ts.ModuleKind.AMD,
        target: ts.ScriptTarget.ES5,
    };
    const sourceFiles = [localStorage.getItem("source")];
    const host = createCompilerHostForBrowser(options, ["./"]);

    return ts.createProgram(sourceFiles, options, host);
}

I loaded the rule this way, which I know is not ideal

import { Rule } from "./rules/noConsoleRule"; /* not exported from tslint; copied to repo and exported */
export default class Linter {
    public static VERSION = "5.8.0";
    /* ... */ 
    private getEnabledRules(
        configuration: IConfigurationFile = DEFAULT_CONFIG,
        isJs: boolean,
    ): IRule[] {
        /* Oh no! The ruleLoader function uses Node APIs. What can be done? */
        return [
            new Rule({
                ruleArguments: [],
                ruleName: "no-console",
                ruleSeverity: "error",
                disabledIntervals: [],
            }),
        ];
    }
    /* ... */
}

Questions

  • Most every configuration-related part of TSLint leverages off-limits Node APIs, especially fs. Is there any reason a default JSON object would not suffice for a browser application?
  • Will the browser need its own implementation of the Formatter class? I'm unfamiliar with that portion of the codebase.
  • As @ajafff mentioned, there's no obvious solution for loading the rules. What's the smartest way to do this?

@aervin
Copy link
Contributor

aervin commented Dec 27, 2017

For those interested, astexplorer.net has already come up with a promising browser implementation. Perhaps we can piggy-back off some of their work to bring the TSLint core rules to the browser.

@johnwiseheart
Copy link
Contributor

Hey everyone. I've built a thing that does this, and currently just waiting to get the repo transferred over to the Palantir organization before making the URL public. I'll update this issue, as well as the main README and other docs when that happens (hopefully soon).

@johnwiseheart
Copy link
Contributor

Announcing the tslint-playground! This should meet your needs - but its still in its infancy so feel free to submit issues if you find that it doesn't support what you're expecting.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants