11

While using the new TypeScript feature, so called ES Dynamic Imports, I am not able to run the code of my isomorphic app on the server side using ts-node.

It seems like the error does not occur when using the webpack module loader which transpiles the code in it's own way and running resulting files in a browser.

The error which I've got:

case 0: return [4 /*yield*/, import("./component/main")];
                             ^^^^^^
SyntaxError: Unexpected token import

Usually TypeScript transpiles the import expression to something like that: Promise.resolve(require("./component/main")), but I can't see it there.

How to fix that? Does it have something common with ts-node? Or there is a "polyfill" for node.js?

My tsconfig.json file:

{
  "compilerOptions": {
    "declaration": false,
    "emitDecoratorMetadata": true,
    "allowJs": false,
    "experimentalDecorators": true,
    "importHelpers": true,
    "inlineSourceMap": false,
    "inlineSources": false,
    "lib": [
      "DOM",
      "ES5",
      "ES6",
      "ES7"
    ],
    "listFiles": false,
    "module": "commonjs",
    "noEmitOnError": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "preserveConstEnums": false,
    "pretty": false,
    "removeComments": false,
    "strict": true,
    "target": "es5"
  }
}

the code:

import * as m from "mithril";

import LayoutComponent from "./component/layout";

const render = (
    layout: m.ComponentTypes<any, any>,
) => ({ tag, attrs }: m.Vnode<any, any>) => m(layout, attrs, m(tag as any, attrs));

export default {
    "/:path...": {
        onmatch: async (args, path) => (await import("./component/main")).default,
        render: render(LayoutComponent),
    },
} as m.RouteDefs;
roomcayz
  • 2,334
  • 4
  • 17
  • 26
  • 1
    To get this to work I had to pass the `--compiler` argument explicitly to ts-node. Make it is using the correct version of typescript. – Aluan Haddad Jul 29 '17 at 15:09
  • I'll try that, so it's not using the project's default/installed in node_modules? – roomcayz Jul 29 '17 at 15:11
  • 1
    it didn't worked, sorry – roomcayz Jul 29 '17 at 15:27
  • I can't believe noone ever had same error before, maybe something is wrong with my `tsconfig.json`? but I can't find any reference of what options affects dynamic import – roomcayz Jul 29 '17 at 15:34
  • Try running TSC from the command line and see what it outputs. There are a few tsconfig options that would affect Dynamic import but they also would have caused failure when using pre-existing constructs – Aluan Haddad Jul 29 '17 at 15:35
  • it may be obvious but `tsc` compiles to the same as `ts-node` - the error line is still `case 0: return [4 /*yield*/, import("./component/main")];`, what are those options, can I have reference? I have heard that `allowJs` affects that somehow, but I don't know if that's true – roomcayz Jul 29 '17 at 15:44
  • `allowJs` shouldn't affect it - that's just for type checking JS files as well. – Michael Fedora Jul 29 '17 at 15:49
  • No, as you suspect, it has nothing to do with `--allowJs`. It has to do with `--module` and `--lib` or `--target`. The first affects how all imports are transpiled, and the latter two can affect it because it requires `Promise` for CommonJS support and `--target` implicitly affects `--lib` unless it is explicitly specified. – Aluan Haddad Jul 29 '17 at 15:50
  • 1
    @edit - updated by `tsconfig.json` – roomcayz Jul 29 '17 at 15:53
  • 1
    `tsc --version` gives you what? – Michael Fedora Jul 29 '17 at 15:54
  • 2
    > tsc "--version" Version 2.4.2, > ts-node "--version" ts-node v3.3.0 node v8.2.1 typescript v2.4.2 – roomcayz Jul 29 '17 at 15:55
  • 1
    `"lib": ["dom", "esnext"]` would be better. Not saying it's the issue but I advise changing it. – Aluan Haddad Jul 29 '17 at 16:05
  • I compiled [this sample code](https://gist.github.com/MichaelFedora/c3b521845a671d3ef9a7399cd97b2514) fine with ts 2.4.2 and your tsconfig and then running it in node, as well as running it with ts-node. I don't know what's happening... does ./component/main exist? (Note, in the sample code, import *returns a promise*, though it shouldn't throw a syntax error it is important to note!) – Michael Fedora Jul 29 '17 at 16:26
  • yep, as I wrote - I'm trying to get that isomorphic, so the code already works in the browser with webpack, but not in the node - it's quite confusing, isn't it? – roomcayz Jul 29 '17 at 16:29
  • 1
    Are you using local (node_modules) tsc/ts-node or global? Are those local/global versions? If you're running linux, does sudo have different version than your local user? etc. That's the only other things I can think of. – Michael Fedora Jul 29 '17 at 16:30
  • local, I've pasted versions from the `node_modules` dir, I'm using the `npm` scripts to do tasks so every single time the local versions are used. – roomcayz Jul 29 '17 at 16:35
  • 1
    @edit I've pasted the code which is used – roomcayz Jul 29 '17 at 16:35
  • 1
    I replicated the error, working on solution now... – Michael Fedora Jul 29 '17 at 16:51

1 Answers1

8

This is a bug in the Typescript Compiler which will be fixed in 2.5.

Exporting a default object with a function that imports a file will not compile the import statement into a require statement in Typescript 2.4.x.

For example, while this:

export const sudo = { run() { return import('./test3'); } }

Will compile to this:

exports.sudo = { run: function () { return Promise.resolve().then(function () { return require('./test3'); }); } };

This:

export default { run() { return import('./test3'); } }

Compiles into this:

exports.default = { run: function () { return import('./test3'); } };

Which is obviously wrong. A temporary solution would be this:

export const sudo = { run() { return import('./test3'); } }

export default sudo;

Which compiles (correctly) into this:

exports.sudo = { run: function () { return Promise.resolve().then(function () { return require('./test3'); }); } };
exports.default = exports.sudo;
Michael Fedora
  • 556
  • 4
  • 10
  • Okay, so in other words I'm forced to use Promise instead of async/await? `import(...).then((cmp) => cmp.default)` should do the trick, am I correct? – roomcayz Jul 29 '17 at 17:16
  • 2
    No; you are forced to export a non-default object, and then export default that object. Async/await will still work (afaik, I can keep trying) – Michael Fedora Jul 29 '17 at 17:16
  • yep, you are right, I can not use that syntax, thanks for explaining that – roomcayz Jul 29 '17 at 17:20
  • 1
    You can still keep the rest of your code the same (using the "default" object, etc), just make sure you are also exporting a sudo-default *first*, and then exporting said sudo-default as the "default" so that it compiles correctly. – Michael Fedora Jul 29 '17 at 17:23
  • 1
    I am on Typescript 2.5.3 (judged from `package.version` and `yarn list`) and the bug is still present. – Frank N Nov 27 '17 at 15:44
  • Seems to be fixed in [2.6](https://github.com/Microsoft/TypeScript/issues/18968). – Michael Fedora Nov 30 '17 at 21:30
  • off-topic question, but what’s the difference between `Promise.resolve().then(() => require('./test3'))` and `Promise.resolve(require('./test3'))`? – chharvey Feb 19 '19 at 05:47
  • the first will run the lambda function when the promise is resolved, the second will run the require function *before* it is resolved, because you are not passing a function there but rather the *result* of the `require()` call. – Michael Fedora Feb 22 '19 at 19:01