question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Pre-Compiler Plugin Proposal

See original GitHub issue

Search Terms

Extension, Plugin, Vue, Custom extensions

I found some issues but no proposal 😕

Suggestion

Hi! I’m an author of the fork-ts-checker-webpack-plugin. Last few months I was busy with rewriting this plugin to pay off the technological debt.

One of the features that this plugin provides is support for the Vue.js Single File Components. I was able to implement it but in order to work with the Watch program and SolutionBuilder I had to do some workarounds. It’s because there is an assertion in the TypeScript source code that files cannot have a custom extension.

The main issue with this approach is that you have to implement these workarounds in every TypeScript’s API client and they can behave differently (like appendTsSuffixTo in the ts-loader). I know that there are already Language Service Plugins, but they don’t work if you use different API, like Watch.

The proposal

A new type of TypeScript plugins that can pre-compile custom source code to the format that TypeScript understands. The simplified interface would be:

////////////////////////////////////////////////
// update existing implementation and typings //

/**
 * Keep it for the backward compatibility
 * @deprecated
 */
interface PluginCreateInfo {
  project: Project;
  languageService: LanguageService;
  languageServiceHost: LanguageServiceHost;
  serverHost: ServerHost;
  config: any;
}
// the new name for this type
type LanguageServicePluginCreateInfo = PluginCreateInfo;

/**
 * Keep it for the backward compatibility
 * @deprecated
 */
interface PluginModule {
  type?: 'language-service-plugin'; // not required due to backward compatibility
  getExternalFiles?(project: Project): string[];
  onConfigurationChanged?(config: any): void;
  create(createInfo: PluginCreateInfo): LanguageService;
}
// the new name for this type
type LanguageServicePluginModule = PluginModule & {
  type: 'language-service-plugin'; // required in the new type
}

/////////////////////////////
// add new type of plugins //

interface PreCompilerPluginModuleCreateInfo {
  moduleResolutionHost: ModuleResolutionHost;
  sourceMapHost: SourceMapHost;
  config: any;
}
interface PreCompilerPluginModule {
  type: 'pre-compiler-plugin'; // to distinguish between LanguageServicePlugin and PreCompilerPlugin
  onConfigurationChanged?(config: any): void;
  create(createInfo: PreCompilerPluginModuleCreateInfo): PreCompilerPlugin;
}
interface PreCompilerPlugin {
  extensions: string[]; // list of supported extensions, for example ['.vue'] or ['.mdx']
  createPreCompiledSourceFile(
    fileName: string,
    sourceText: string,
    languageVersion: ScriptTarget,
    setParentNodes?: boolean
  ): PreCompiledSourceFile;
}
// we need to define an extended version of the SourceFile to support additional sourceMap
interface PreCompiledSourceFile extends SourceFile {
  preSourceMapText: string; // we need to provide source map to calculate diagnostics positions and source maps
}

// we need to define a function to create a PreCompiledSourceFile 
// (as we can't infer ScriptKind so it's not an optional parameter)
function createPreCompiledSourceFile(
  fileName: string,
  sourceText: string,
  languageVersion: ScriptTarget,
  scriptKind: ScriptKind,
  setParentNodes?: boolean,
  sourceMapText?: string
): PreCompiledSourceFile

// this will help with source map generation
interface SourceTextNavigator {
  getLineAndCharacterOfPosition(position: number): LineAndCharacter;
  getPositionOfLineAndCharacter(line: number, character: number): number;
  getLineEndOfPosition(position: number): number;
  getLineStarts(): number[];
}
interface SourceMapHost {
  createSourceMapGenerator(fileName: string): SourceMapGenerator;
  createSourceTextNavigator(text: string): SourceTextNavigator;
}


////////////////////////////////////////////////
// update existing implementation and typings //

type AnyPluginModule = PluginModule | PreCompilerPluginModule;

interface PluginModuleWithName {
  name: string;
  module: AnyPluginModule;
}
type PluginModuleFactory = (mod: {
  typescript: typeof ts;
}) => AnyPluginModule

This architecture would allow adding new plugin types in the future (so we could add ModuleResolutionPlugin to support Yarn PnP in the future)

The maintenance cost of this feature should be low because the exposed API is pretty simple.

There is a point in the TypeScript Design Goals

  1. Provide an end-to-end build pipeline. Instead, make the system extensible so that external tools can use the compiler for more complex build workflows.

If you think that this feature is incompatible with this point, we could limit it to emitting only .d.ts files.

I could try to implement it, but first I need to know if it’s something that you would like to add to the TypeScript and if this API is a good direction 😃

Use Cases

I imagine that community would create PreCompilerPlugins for a lot of use cases. For example:

  • vue-typescript-plugin - support for the Single File Components
  • mdx-typescript-plugin - support for embedded TypeScript in the MDX files
  • graphql-typescript-pluging - support for generation of GraphQL types / clients in the runtime (so we don’t have to use generators anymore)
  • css-modules-typescript-plugin - support for typed css modules (instead of declare module '*.css';)

Examples

tsconfig.json

{
  "compilerOptions": {
    "target": "ES6",
    "module": "CommonJS",
    "lib": ["ES6"],
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "baseUrl": "./src",
    "outDir": "./lib",
    "plugins": [{ "name": "vue-typescript-plugin" }]
  },
  "include": ["./src"],
  "exclude": ["node_modules"]
}

node_modules/vue-typescript-plugin/index.ts

It’s a pseudo-code based on the fork-ts-checker-webpack-plugin implementation. It adds support for the .vue files.

import compiler from 'vue-template-compiler';

function init(modules: { typescript: typeof import("typescript/lib/typescript") }) {
  const ts = modules.typescript;

  function create({ sourceMapHost }: ts.PreCompilerPluginModuleCreateInfo): ts.PreCompilerPlugin {
    function getScriptKindByLang(lang: string | undefined) {
      switch (lang) {
        case 'ts':
          return ts.ScriptKind.TS;
        case 'tsx':
          return ts.ScriptKind.TSX;
        case 'jsx':
          return ts.ScriptKind.JSX;
        case 'json':
          return ts.ScriptKind.JSON;
        case 'js':
        default:
          return ts.ScriptKind.JS;
      }
    }

    function createNoScriptSourceFile(
      fileName: string,
      sourceText: string,
      languageVersion: ts.ScriptTarget,
      setParentNodes: boolean | undefined
    ): ts.PreCompilerSourceFile {
      const compiledText = 'export default {};';

      // generate source map
      const sourceMap = sourceMapHost.createSourceMapGenerator(fileName);
      const sourceIndex = sourceMap.addSource(fileName);
      sourceMap.setSourceContent(sourceIndex, sourceText);

      const sourceTextNavigator = sourceMapHost.createSourceTextNavigator(sourceText);

      const sourceStart = sourceTextNavigator.getLineAndCharacterOfPosition(0);
      const sourceEnd = sourceTextNavigator.getLineAndCharacterOfPosition(sourceText.length);

      sourceMap.addMapping(0, 0, sourceIndex, sourceStart.line, sourceStart.character);
      sourceMap.addMapping(0, compiledText.length, sourceIndex, sourceEnd.line, sourceEnd.character);

      // create source file
      return ts.createPreCompiledSourceFile(
        fileName,
        'export default {};',
        languageVersion,
        ts.ScriptKind.JS,
        setParentNodes
      );
    }

    function createSrcScriptSourceFile(
      fileName: string,
      sourceText: string,
      languageVersion: ts.ScriptTarget,
      setParentNodes: boolean | undefined,
      sourceStartPosition: number,
      sourceEndPosition: number,
      scriptTagSrc: string,
      scriptTagLang: string | undefined,
    ): ts.PreCompilerSourceFile {
      // import path cannot be end with '.ts[x]'
      const compiledText = `export * from "${scriptTagSrc.replace(/\.tsx?$/i, '')}";`;

      // generate source map
      const sourceMap = sourceMapHost.createSourceMapGenerator(fileName);
      const sourceIndex = sourceMap.addSource(fileName);
      sourceMap.setSourceContent(sourceIndex, sourceText);

      const sourceTextNavigator = sourceMapHost.createSourceTextNavigator(sourceText);
      const sourceStart = sourceTextNavigator.getLineAndCharacterOfPosition(sourceStartPosition);
      const sourceEnd = sourceTextNavigator.getLineAndCharacterOfPosition(sourceEndPosition);

      sourceMap.addMapping(0, 0, sourceIndex, sourceStart.line, sourceStart.character);
      sourceMap.addMapping(0, compiledText.length, sourceIndex, sourceEnd.line, sourceEnd.character);

      // create source file
      return ts.createPreCompiledSourceFile(
        fileName,
        compiledText,
        languageVersion,
        getScriptKindByLang(scriptTagLang),
        setParentNodes,
        sourceMap.toString()
      );
    }

    function createInlineScriptSourceFile(
      fileName: string,
      sourceText: string,
      languageVersion: ts.ScriptTarget,
      setParentNodes: boolean | undefined,
      sourceStartPosition: number,
      scriptTagContent: string,
      scriptTagLang: string | undefined
    ): ts.PreCompilerSourceFile {
      const compiledText = sourceTagContent;

      // generate source map
      const sourceMap = sourceMapHost.createSourceMapGenerator(fileName);
      const sourceIndex = sourceMap.addSource(fileName);
      sourceMap.setSourceContent(sourceIndex, sourceText);

      const sourceTextNavigator = sourceMapHost.createSourceTextNavigator(sourceText);
      const compiledTextNavigator = sourceMapHost.createSourceTextNavigator(compiledText);

      const compiledLineStarts = compiledTextNavigator.getLineStarts();
      compiledLineStarts.forEach((compiledLineStart) => {
        // map line by line
        const sourceStart = sourceTextNavigator.getLineAndCharacterOfPosition(sourceStartPosition + compiledLineStart);
        const sourceEnd = sourceTextNavigator.getLineAndCharacterOfPosition(sourceTextNavigator.getLineEndOfPosition(sourceStartPosition + compiledLineStart));
        const compiledStart = compiledTextNavigator.getLineAndCharacterOfPosition(compiledLineStart);
        const compiledEnd = compiledTextNavigator.getLineAndCharacterOfPosition(compiledTextNavigator.getLineEndOfPosition(compiledLineStart));

        sourceMap.addMapping(compiledStart.line, compiledStart.character, sourceIndex, sourceStart.line, sourceStart.character);
        sourceMap.addMapping(compiledEnd.line, compiledEnd.character, sourceIndex, sourceEnd.line, sourceEnd.character);
      });

      // create source file
      return ts.createPreCompiledSourceFile(
        fileName,
        compiledText,
        languageVersion,
        getScriptKindByLang(scriptTagLang),
        setParentNodes,
        sourceMap.toString()
      );
    }


    return {
      extensions: ['.vue'],
      createPreCompiledSourceFile(fileName, sourceText, languageVersion, setParentNodes) {
        const { script } = compiler.parseComponent(sourceText, { pad: 'space' });
    
        if (!script) {
          // No <script> block
          return createNoScriptSourceFile(fileName, sourceText, languageVersion, setParentNodes);
        } else if (script.attrs.src) {
          // <script src="file.ts" /> block
          return createSrcScriptSourceFile(
            fileName, 
            sourceText, 
            languageVersion, 
            setParentNodes, 
            script.start, 
            script.end, 
            script.attrs.src, 
            script.attrs.lang
          );
        } else {
          // <script lang="ts"></script> block
          return createInlineScriptSourceFile(
            fileName, 
            sourceText, 
            languageVersion, 
            setParentNodes, 
            script.start,
            sourceText.slice(script.start, script.end), 
            script.attrs.lang
          );
        }
      }
    }
  }

  return {
    type: 'pre-compiler-plugin',
    create
  };
}

export = init;

Checklist

My suggestion meets these guidelines:

  • This wouldn’t be a breaking change in existing TypeScript/JavaScript code
  • This wouldn’t change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn’t a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript’s Design Goals.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:63
  • Comments:13 (5 by maintainers)

github_iconTop GitHub Comments

11reactions
armano2commented, Jan 19, 2021

plugins systems are not normal for compilers.

your claim is not true as there is a lot of compilers that support plugins, most notable one is gcc

there is more, but this short list should be enough


@orta do you have any updates about compile time plugins? this feature was requested few years ago

7reactions
piotr-olescommented, Oct 16, 2020

@orta Do you have any update regarding this feature request? 😃

Read more comments on GitHub >

github_iconTop Results From Across the Web

Precompiling assets failed - remote rejected - Stack Overflow
The error says that loose is false for the preset-env and true for plugin-proposal-class-properties . I can see you have "loose": true for ......
Read more >
babel-plugin-htmlbars-inline-precompile - Package Manager
Babel plugin to replace tagged .hbs formatted strings with a precompiled version. Requirements. Node 8+; Ember 2.10+; Babel 7. Usage. Can be used...
Read more >
babel-plugin-htmlbars-inline-precompile - npm package | Snyk
Babel plugin to replace tagged template strings with precompiled HTMLBars templates For more information about how to use this package see README.
Read more >
Gradle project with precompiled script plugins cannot be ...
gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52) at org.gradle.execution.plan.LocalTaskNodeExecutor.execute ...
Read more >
record-dot-preprocessor - Hackage
field syntax. [ bsd3, development, library, program ] [ Propose Tags ]. In almost every programming language a.b will get ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found