Ts-loader: Multiple TS config/projects, entry files, and shared code

Created on 28 Sep 2017  ·  23Comments  ·  Source: TypeStrong/ts-loader

I have 3 dirs:

- service-worker
- browser
- shared

service-worker and browser have their own tsconfig.json. This is necessary because I need to specify a different lib compiler option to files in these projects, as they are ran in different environments (service workers and DOM/window/browser, respectively).

I initially thought I could just specify multiple entry points and ts-loader would use the correct tsconfig.json relative to each entry point. Any files it discovers in the dependency graph of the entry files would use the same configuration as the respective entry file.

Unfortunately this won't work—ts-loader will re-use the first TypeScript instance (and therefore config) it creates for both entry points.

I then discovered ts-loader's instance option, which forces ts-loader to create different instances. However, the "shared" code is difficult to place: it can be compiled with either TS configs!

In any case, below is what I have ended up with. Have you got any ideas how I could clean this up? Or how this might be made easier in the future?

Ideally, this would be as simple as tsc makes it. Feed it a configuration file, and it will discover the entry file(s) through files and compile all files in the dependency graph with the same configuration.

import * as path from 'path';
import * as webpack from 'webpack';

const SOURCE_PATH = path.join(__dirname, '..');
const SHARED_SOURCE_PATH = path.join(SOURCE_PATH, './src/shared');
const BROWSER_SOURCE_PATH = path.join(SOURCE_PATH, './src/browser');
const SERVICE_WORKER_SOURCE_PATH = path.join(SOURCE_PATH, './src/service-worker');

const config: webpack.Configuration = {
    devtool: 'source-map',
    entry: {
        browser: path.join(BROWSER_SOURCE_PATH, './main.ts'),
        'service-worker': path.join(SERVICE_WORKER_SOURCE_PATH, './index.ts'),
    },
    output: {
        path: path.join(SOURCE_PATH, './target'),
        filename: '[name].js',
    },
    resolve: {
        extensions: [
            // start defaults
            '.js',
            '.json',
            // end defaults
            '.ts',
        ],
    },
    module: {
        rules: [
            {
                test: /\.ts$/,
                include: [SHARED_SOURCE_PATH],
                loader: 'ts-loader',
                options: {
                    instance: 'shared',
                },
            },
            {
                test: /\.ts$/,
                include: [BROWSER_SOURCE_PATH],
                loader: 'ts-loader',
                options: {
                    instance: 'browser',
                },
            },
            {
                test: /\.ts$/,
                include: [SERVICE_WORKER_SOURCE_PATH],
                loader: 'ts-loader',
                options: {
                    instance: 'service-worker',
                },
            },
        ],
    },
};

export default config;

Most helpful comment

Could you share the solution for others please? Always helps!

All 23 comments

That's not the worst of configs even if it's a bit verbose. Completely on a side note but I'd been thinking about how ts-loader might well be used with service workers (I love a PWA) and this is a nice approach.

I initially thought I could just specify multiple entry points and ts-loader would use the correct tsconfig.json relative to each entry point.

See #267 - this is not supported at present although if @tkrotoff has a go then it could happen and that might improve your config.

Do you want to try using the configFile option as suggested by @dbettini in #54:

{
  test: /\.tsx?$/,
  include: "./path/to/entry/index.ts",
  use: [
    {
      loader: "awesome-typescript-loader",
      options: {
        instance: "myInstanceName",
        configFileName: "./path/to/entry/tsconfig.json"
      }
    },
    "angular-router-loader",
    "angular2-template-loader"
  ]
}

I believe ts-loader should support this too

The "shared" code doesn't have a TS project of its own, but it is implicitly part of the dependency graph of both the "browser" and "service worker" TS projects. However, using the configuration above, webpack/ts-loader will attempt to find a unique TS config file for "shared", leading to errors:

❯ webpack --config ./target/webpack.config.js
ts-loader: Using [email protected] and /Users/OliverJAsh/Development/twitter-paper/src/browser/tsconfig.json
ts-loader: Using [email protected] and /Users/OliverJAsh/Development/twitter-paper/src/service-worker/tsconfig.json
ts-loader: Using [email protected]
Hash: 9f69766a7184ab47e3c3
Version: webpack 3.6.0
Time: 2317ms
                Asset     Size  Chunks             Chunk Names
    service-worker.js  68.5 kB       0  [emitted]  service-worker
           browser.js   5.6 kB       1  [emitted]  browser
service-worker.js.map  87.4 kB       0  [emitted]  service-worker
       browser.js.map  8.34 kB       1  [emitted]  browser
   [4] ./src/browser/index.ts 3.06 kB {1} [built]
   [5] ./src/service-worker/index.ts 1.62 kB {0} [built]
  [17] ./src/shared/types.ts 94 bytes {0} [built] [failed] [1 error]
    + 15 hidden modules

ERROR in ./src/shared/types.ts
Module build failed: error while parsing tsconfig.json
 @ ./src/service-worker/index.ts 5:16-42

If I remove the rule for "shared", it will fail to compile the TypeScript altogether:

❯ webpack --config ./target/webpack.config.js
ts-loader: Using [email protected] and /Users/OliverJAsh/Development/twitter-paper/src/browser/tsconfig.json
ts-loader: Using [email protected] and /Users/OliverJAsh/Development/twitter-paper/src/service-worker/tsconfig.json
Hash: 3f14b3c24b6945bfe3ca
Version: webpack 3.6.0
Time: 2446ms
                Asset     Size  Chunks             Chunk Names
    service-worker.js  68.7 kB       0  [emitted]  service-worker
           browser.js   5.6 kB       1  [emitted]  browser
service-worker.js.map  87.4 kB       0  [emitted]  service-worker
       browser.js.map  8.34 kB       1  [emitted]  browser
   [4] ./src/browser/index.ts 3.06 kB {1} [built]
   [5] ./src/service-worker/index.ts 1.62 kB {0} [built]
  [17] ./src/shared/types.ts 297 bytes {0} [built] [failed] [1 error]
    + 15 hidden modules

ERROR in ./src/shared/types.ts
Module parse failed: /Users/OliverJAsh/Development/twitter-paper/src/shared/types.ts Unexpected token (3:7)
You may need an appropriate loader to handle this file type.
| import * as t from 'io-ts';
|
| export enum PushEventTypeT {
|     publication = 'publication',
| }
 @ ./src/service-worker/index.ts 5:16-42

Ideally, when webpack traces an entry file, it will re-use the same TS config for the dependency graph of that entry file. This is what happens when I run tsc and specify a config file. In this case that would mean "shared" is type checked against the config of both projects.

I can workaround this by creating a TS config file for "shared", however this means my IDE/tsc configuration will be different from my webpack configuration. 🤔

Try setting your rules like this:

rules: [
            {
                test: /\.ts$/,
                include: [BROWSER_SOURCE_PATH, SHARED_SOURCE_PATH],
                loader: 'ts-loader',
                options: {
                    instance: 'browser',
                    configFileName: "./path/to/browser/tsconfig.json"
                },
            },
            {
                test: /\.ts$/,
                include: [SERVICE_WORKER_SOURCE_PATH, SHARED_SOURCE_PATH],
                loader: 'ts-loader',
                options: {
                    instance: 'service-worker',
                    configFileName: "./path/to/service-worker/tsconfig.json"
                },
            },
        ],

@dbettini That does the trick! Using configFile instead of configFileName as the latter is deprecated. Thank you!

One improvement could be to remove the need for instance if the configFiles are different 🤔

@OliverJAsh just to be sure, did you only add the configFileName, or did you also need to add SHARED_SOURCE_PATH to include?

@dbettini I added configFile and SHARED_SOURCE_PATH in include:

import * as path from 'path';
import * as webpack from 'webpack';

const ROOT_PATH = path.join(__dirname, '..');
const TARGET_PATH = path.join(ROOT_PATH, './target-webpack');

const SHARED_SOURCE_PATH = path.join(ROOT_PATH, './src/shared');
const BROWSER_SOURCE_PATH = path.join(ROOT_PATH, './src/browser');
const SERVICE_WORKER_SOURCE_PATH = path.join(ROOT_PATH, './src/service-worker');

const config: webpack.Configuration = {
    devtool: 'source-map',
    entry: {
        browser: path.join(BROWSER_SOURCE_PATH, './index.ts'),
        'service-worker': path.join(SERVICE_WORKER_SOURCE_PATH, './index.ts'),
    },
    output: {
        path: TARGET_PATH,
        filename: '[name].js',
    },
    resolve: {
        extensions: [
            // start defaults
            '.js',
            '.json',
            // end defaults
            '.ts',
        ],
    },
    module: {
        rules: [
            {
                test: /\.ts$/,
                include: [BROWSER_SOURCE_PATH, SHARED_SOURCE_PATH],
                loader: 'ts-loader',
                options: {
                    instance: 'browser',
                    configFile: path.join(BROWSER_SOURCE_PATH, 'tsconfig.json'),
                },
            },
            {
                test: /\.ts$/,
                include: [SERVICE_WORKER_SOURCE_PATH, SHARED_SOURCE_PATH],
                loader: 'ts-loader',
                options: {
                    instance: 'service-worker',
                    configFile: path.join(SERVICE_WORKER_SOURCE_PATH, 'tsconfig.json'),
                },
            },
        ],
    },
};

export default config;

@OliverJAsh yep, as expected. The reason it's needed is because webpack never had .ts rules set for your shared folder. And using a third shared instance was not an option, because your shared folder is not an entry point, so webpack wouldn't build it even if you did have a tsconfig.json in it. What was needed was giving webpack access to your shared folder in both of your instances that build your entries, because it is their dependency, but since it's in the same directory level, you need to add it to include.

@dbettini Just spotted a problem: for some reason, webpack says there is an error but provides no details why (using the config above):

ts-loader: Using [email protected] and /Users/OliverJAsh/Development/twitter-paper/src/browser/tsconfig.json
ts-loader: Using [email protected] and /Users/OliverJAsh/Development/twitter-paper/src/service-worker/tsconfig.json
Hash: 005abdd80603e0885980
Version: webpack 3.6.0
Time: 3159ms
                Asset     Size  Chunks             Chunk Names
    service-worker.js  69.5 kB       0  [emitted]  service-worker
           browser.js   5.6 kB       1  [emitted]  browser
service-worker.js.map  89.8 kB       0  [emitted]  service-worker
       browser.js.map  8.34 kB       1  [emitted]  browser
   [5] ./src/browser/index.ts 3.06 kB {1} [built] [1 error]
   [6] ./src/service-worker/index.ts 1.62 kB {0} [built]
  [17] ./src/shared/types.ts 1.07 kB {0} [built]
    + 15 hidden modules

Note the "[1 error]" beside /src/browser/index.ts.

If I enable only one entry point and the corresponding rule entry, neither "browser" or "service-worker" will error. But when I run both using the above config, webpack reports an error for the file "/src/browser/index.ts" but with no further details.

@johnnyreilly Do you have any ideas here? Is there a way to log more info about the error?

@OliverJAsh like I said, I use at-loader, I always get all errors logged by default when using it. If ts-loader doesn't log errors by default, maybe you can try changing logLevel

It should log all errors - I'd advise forking ts-loader and adding in some extra logging to diagnose. Do you have a minimal repro repo you could share?

@johnnyreilly Here is a minimal repo: https://github.com/OliverJAsh/webpack-ts-loader-shared

For some reason, specifying "module": "es2015" compiler option in both tsconfig.jsons will fix the error. I don't really understand why though 🤔 https://github.com/OliverJAsh/webpack-ts-loader-shared/tree/es-module-compiler-option

In any case, it would be good to get the error message printed.

One problem with the suggested config above is the "shared" code will only be type checked once, using whichever rule is found first. Both "browser" and "service-worker" include "shared", but when webpack compiles "shared", it won't use the tsconfig.json corresponding to that entry file—it will just use the tsconfig.json instance of the first matching rule. @dbettini

Edit: upon further testing, this is wrong. Ignore this comment.


~I just gave this a go with awesome-typescript-loader. A few things to note:~

  • "shared" will be type checked against both tsconfig.jsons, not just the first matching
  • In my webpack config I don't have to include "shared" in either rules. Somehow at-loader knows to use the same tsconfig.json for all files in the dependency graph of the entry file

Example: https://github.com/OliverJAsh/webpack-ts-loader-shared/tree/awesome-typescript-loader

Here is an example using at-loader where "shared" is type checked for both tsconfig.jsons, and will fail for one of them: https://github.com/OliverJAsh/webpack-ts-loader-shared/tree/awesome-typescript-loader-shared-type-check

Sorry, does it work with at or not?

The module thing might be because (at present) ts-loader has different module defaults to tsc. We plan to rectify this with V3 (being worked on now - see #637)

At this time I can't say it does work any better with at-loader—sorry to cause confusion!

No worries :smile:

Closing as a solution was found (with trade offs). Thanks again.

Could you share the solution for others please? Always helps!

~3 years later, sorry! My solution was just to use one tsconfig.json.

(We still want to keep separate projects, but project references still have issues with VS Code.)

Haha! Props for replying after 3 years 😄

Thought I'd keep this one alive just a bit longer; I'm running ts-loader alongside serverless framework, specifically the serverless-webpack plugin, which works fine, and FWIW has a helper for declaring multiple entrypoints (one for each lambda). These compile fine when used at the root project (~1s) incremental compilation, but when ANY sub project gets changed, it emits changes for each of the entrypoints, causing a whole sea of recompilations to occur.

Any thoughts on what I can do to prevent that from happening?

Was this page helpful?
0 / 5 - 0 ratings