Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement an official TypeScript compiler plugin. #10610

Merged
merged 2 commits into from Jul 10, 2019

Conversation

@benjamn
Copy link
Member

commented Jul 7, 2019

As requested in meteor/meteor-feature-requests#285.

This plugin package enables the use of TypeScript modules with .ts or .tsx file extensions in Meteor applications and packages, alongside .js modules (or whatever other types of modules you might be using).

Usage

The typescript package registers a compiler plugin that transpiles TypeScript to plain ECMAScript, which is then compiled by Babel for multiple targets (server, modern browsers, legacy browsers, cordova). By default, the typescript package is included in the .meteor/packages file of all new Meteor applications.

To add the typescript package to an existing app, run the following command from your app directory:

meteor add typescript

To add the typescript package to an existing package, include the statement api.use('typescript'); in the Package.onUse callback in your package.js file:

Package.onUse(function (api) {
  api.use('typescript');
});

Supported TypeScript features

Almost all TypeScript syntax is supported, though this plugin currently does not attempt to provide type checking (just compilation).

Since the Meteor typescript package runs the official TypeScript compiler before running Babel, it does not suffer from the same caveats known to affect the Babel TypeScript transform. In particular, namespaces are fully supported.

However, as of this writing, the Meteor typescript package compiles TypeScript modules individually, using the transpileModule function, which means that certain cross-file analysis and compilation will not work. For example, export const enum {...} is not fully supported, though const enum {...} works when confined to a single module.

Unlike the Babel implementation of TypeScript, there is nothing fundamentally preventing Meteor from compiling all TypeScript modules together, rather than individually, which would enable full support for features like export const enum. That's an area for future improvement, though we will have to weigh the performance implications carefully.

As of this writing, tsconfig.json files are ignored by the plugin. There's nothing fundamentally preventing the Meteor typescript plugin from accepting configuration options from tsconfig.json, but for now we've kept things simple by avoiding configuration complexities. You may still want to have a tsconfig.json file in your application root directory to configure the behavior of editors like VSCode, but it will not be respected by Meteor.

Finally, since the TypeScript compiler runs first, syntax that is not understood by the TypeScript compiler, such as experimental ECMAScript proposals, may cause the TypeScript parser to reject your code. You can use .babelrc files to configure Babel compilation, but TypeScript still
has to accept the code first.

Areas for improvement

  • Compile all TypeScript modules at the same time, rather than compiling them individually, to enable cross-module analysis and compilation. In the meantime, if you need this behavior, consider using adornis:typescript.

  • Use the version of typescript installed in the application node_modules directory, rather than the one bundled with meteor-babel. We will attempt to keep meteor-babel's version of typescript up-to-date, but it would be better to leave this decision to the application developer.

  • Generate .d.ts declaration files that can be consumed by external tools. In particular, a Meteor package that uses TypeScript could generate .d.ts files in the /node_modules/meteor/package-name/ directory, which would allow tools like VSCode to find the right types for the package when importing from meteor/package-name.

  • Cache the output of the TypeScript compiler separately from the output of Meteor's Babel compiler pipeline. The TypeScript compiler does not compile code differently for different targets (server, modern, legacy, etc.), so its results could theoretically be reused for all targets.

  • Make the typescript plugin look up tsconfig.json files (similar to how babel-compiler looks up .babelrc files) and obey (some of) the configuration options.

@benjamn benjamn added this to the Release 1.8.2 milestone Jul 7, 2019

@benjamn benjamn self-assigned this Jul 7, 2019

}, function () {
return new TypeScriptCompiler({
react: true,
typescript: true,

This comment has been minimized.

Copy link
@benjamn

benjamn Jul 7, 2019

Author Member

See meteor/babel#25 for details about how this is implemented in meteor-babel.

benjamn added some commits Jul 6, 2019

@benjamn benjamn force-pushed the core-typescript-plugin branch from a5a0079 to 01fb509 Jul 7, 2019

@yorrd

This comment has been minimized.

Copy link

commented Jul 8, 2019

@benjamn glad you're working on this 😍 . I see you decided on implementing your own caching. At adornis, we were evaluating whether to implement support for incremental builds from typescript itself. Do you think that could have performance benefits over caching on a file by file basis? We've been using our predecessors caching and rebuild times get really long if a lot of files import that specific file which they don't (at least not in that extent) when using tsc. Haven't investigated this further, just random thoughts...

EDIT: thanks for the shoutout :)

@benjamn benjamn merged commit c1cd8fc into release-1.8.2 Jul 10, 2019

19 checks passed

CLA Author has signed the Meteor CLA.
Details
ci/circleci: Clean Up Your tests passed on CircleCI!
Details
ci/circleci: Docs Your tests passed on CircleCI!
Details
ci/circleci: Get Ready Your tests passed on CircleCI!
Details
ci/circleci: Isolated Tests Your tests passed on CircleCI!
Details
ci/circleci: Test Group 0 Your tests passed on CircleCI!
Details
ci/circleci: Test Group 1 Your tests passed on CircleCI!
Details
ci/circleci: Test Group 10 Your tests passed on CircleCI!
Details
ci/circleci: Test Group 2 Your tests passed on CircleCI!
Details
ci/circleci: Test Group 3 Your tests passed on CircleCI!
Details
ci/circleci: Test Group 4 Your tests passed on CircleCI!
Details
ci/circleci: Test Group 5 Your tests passed on CircleCI!
Details
ci/circleci: Test Group 6 Your tests passed on CircleCI!
Details
ci/circleci: Test Group 7 Your tests passed on CircleCI!
Details
ci/circleci: Test Group 8 Your tests passed on CircleCI!
Details
ci/circleci: Test Group 9 Your tests passed on CircleCI!
Details
continuous-integration/appveyor/pr AppVeyor build succeeded
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details

@benjamn benjamn deleted the core-typescript-plugin branch Jul 15, 2019

@yorrd

This comment has been minimized.

Copy link

commented Jul 16, 2019

@benjamn how do you do absolute imports? 'imports/client/my-component' results in module not found. The exact same code worked with our old plugin. We have like 200 absolute imports, please don't make us change them :D

@gerwinbrunner

This comment has been minimized.

Copy link
Contributor

commented Jul 26, 2019

@benjamn @yorrd
I have the same issue with the imports. Did you find a solution?

@benjamn

This comment has been minimized.

Copy link
Member Author

commented Jul 26, 2019

The nice thing about using Babel for part of the compilation pipeline is that we can use a Babel plugin to rewrite imported module specifiers.

I think we can automate this better, but here's a custom plugin that I wrote for one of our internal application development teams:

const fs = require('fs');
const { dirname, join } = require('path');

const appDir = dirname(dirname(__dirname));
const hasOwn = Object.prototype.hasOwnProperty;

// These directory names are computed only once per build process, so you
// may need to restart Meteor to pick up any changes.

const nodeModulesDirNames = Object.create(null);
fs.readdirSync(join(appDir, 'node_modules')).forEach((dir) => {
  if (!dir.startsWith('.')) {
    nodeModulesDirNames[dir] = true;
  }
});

const topLevelDirNames = Object.create(null);
fs.readdirSync(appDir).forEach((item) => {
  if (!item.startsWith('.') && item !== 'node_modules') {
    const stat = fs.statSync(item);
    if (stat.isDirectory()) {
      topLevelDirNames[item] = stat;
    }
  }
});

// Babel plugin that rewrites import declarations like
//
//   import foo from "imports/bar"
//
// to use properly absolute identifier strings like
//
//   import foo from "/imports/bar"
//
// TypeScript can understand imports/bar thanks to the "paths" property in
// tsconfig.json, but Node and Meteor treat imports/bar as referring to a
// package in node_modules called "imports", which does not exist.
//
// If a directory name exists in both node_modules and the root
// application directory, the node_modules package will take precedence.

module.exports = function plugin(api) {
  function helper(path) {
    // An ImportDeclaration will always have a source, but an
    // ExportAllDeclaration or ExportNamedDeclaration may not.
    const { source } = path.node;
    if (!source) return;

    const sourceId = source.value;
    const name = sourceId.split('/', 1)[0];

    // If the first component of the sourceId is a top-level directory in
    // the application, and not the name of a directory in node_modules,
    // prepend a leading / to make it an absolute identifier that Meteor's
    // module system can understand.
    if (
      hasOwn.call(topLevelDirNames, name) &&
      !hasOwn.call(nodeModulesDirNames, name)
    ) {
      path.get('source').replaceWith(api.types.stringLiteral(`/${sourceId}`));
    }
  }

  return {
    name: 'transform-non-relative-imports',
    visitor: {
      ImportDeclaration: helper,
      ExportAllDeclaration: helper,
      ExportNamedDeclaration: helper,
    },
  };
};

Until we implement this officially, you can put this code in a local file (one that is not eagerly loaded by Meteor), and then refer to it in the plugins section of a .babelrc file.

@yorrd

This comment has been minimized.

Copy link

commented Aug 5, 2019

@benjamn @gerwinbrunner I must be missing something. I included the file in the babelrc (which is otherwise empty) and it seems like the paths are being replaced correctly. However, there are still exceptions failing to import imports/..., so those changed paths somehow don't make it to the compiler. Of course, I tried resetting and stuff,

This is my package file, am I missing something?

meteor-base@1.4.0             # Packages every Meteor app needs to have
mobile-experience@1.0.5       # Packages for a great mobile UX
mongo@1.8.0-alpha190.9                   # The database Meteor supports right now
reactive-var@1.0.11            # Reactive variable for tracker
tracker@1.2.0                 # Meteor's client-side reactive programming library

standard-minifier-css@1.5.3
standard-minifier-js@2.4.1

dynamic-import@0.5.1
server-render@0.3.1
ecmascript
es5-shim@4.8.0
jagi:astronomy
static-html
accounts-password@1.5.1
ostrio:files
email@1.2.3
alanning:roles
jagi:astronomy-timestamp-behavior
fourseven:scss@4.9.0
nimble:restivus
okgrow:analytics
raix:push
promise@0.11.2
babel-runtime@1.4.0-beta182.17
typescript
@coagmano

This comment has been minimized.

Copy link
Contributor

commented Aug 5, 2019

Does it help to specify the root path in the tsconfig?
Something like this (untested):

{
  "compilerOptions" : {
    "baseUrl": "./",
    "paths" : {
      "*": ["./*"]
    }
  }
}

More info on TS module resolution here: microsoft/TypeScript#5039

@yorrd

This comment has been minimized.

Copy link

commented Aug 6, 2019

@coagmano thanks for the idea! Already have those options though

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants
You can’t perform that action at this time.