Search code examples
javascripttypescriptleaflet

Typescript auto generated JS file: "Uncaught TypeError: Failed to resolve module specifier"


Currently working on a Blazor project where I want advanced mapping functionality, using the Leaflet.js library with typescript bindings.

I have added leaflet and @types/leaflet as node modules for allowing typescript support.

With everything ready and running, the browser console shows the following error:

Uncaught TypeError: Failed to resolve module specifier "leaflet". Relative references must start with either "/", "./", or "../".

There is one line generated at the top of the JS file:

import * as L from 'leaflet';

If I remove this line, everything works, but I can't remove it manually because it's generated automatically from my TS file, where it's needed.

I suspect my mistake is in tsconfig, or in the typescript file itself.

tsconfig:

{
  "compileOnSave": true,
  "compilerOptions": {
    "noImplicitAny": false,
    "noEmitOnError": true,
    "removeComments": false,
    "sourceMap": true,
    "target": "ES6",
    "strict": true,
    "rootDir": "typescript",
    "outDir": "wwwroot/scripts",
    "esModuleInterop": true,
  },
  "exclude": [
    "node_modules",
    "wwwroot"
  ],
}

Typescript file:

import * as L from 'leaflet'

let map: L.Map;
let apiKey: string = "";
let mapStyle: string = 'saitken88/cl2vcjae400aw14qoiawg02c8'
let centreLatLong: L.LatLngExpression = [51.509865, -0.118092];

let mapOptions: L.MapOptions = {
    minZoom: 6,
    maxZoom: 9,
    center: centreLatLong,
    zoom: 6,
    attributionControl: false,
};

function initMap(mapId: string) {
    console.log('init map');
    map = L.map(mapId, mapOptions).setView(centreLatLong, 13);

    L.tileLayer(`https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}`, {
        maxZoom: 18,
        id: mapStyle,
        tileSize: 512,
        zoomOffset: -1,
        accessToken: apiKey,
    }).addTo(map);
}

Compiled JS file:

import * as L from 'leaflet';
let map;
let apiKey = "";
let mapStyle = 'saitken88/cl2vcjae400aw14qoiawg02c8';
let centreLatLong = [51.509865, -0.118092];
let mapOptions = {
    minZoom: 6,
    maxZoom: 9,
    center: centreLatLong,
    zoom: 6,
    attributionControl: false,
};
function initMap(mapId) {
    console.log('init map');
    map = L.map(mapId, mapOptions).setView(centreLatLong, 13);
    L.tileLayer(`https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}`, {
        attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
        maxZoom: 18,
        id: mapStyle,
        tileSize: 512,
        zoomOffset: -1,
        accessToken: apiKey,
    }).addTo(map);
}
//# sourceMappingURL=property-map.js.map

index.html scripts:

    <script src="scripts/maps/leaflet.js" type="module"></script>
    <script src="scripts/maps/property-map.js" type="module"></script>

Not sure if this is needed but this is my packake.json

{
  "version": "1.0.0",
  "name": "asp.net",
  "private": true,
  "type": "module",
  "devDependencies": {
    "@types/leaflet": "^1.7.9"
  },
  "dependencies": {
    "leaflet": "^1.8.0"
  }
}

I understand webpack might be a solution here, and have tried it, but frankly it seemed overly cumbersome for just getting one typescript file to compile nicely.


Solution

  • The TypeScript compiler (tsc), just by itself, does not perform any bundling (on the contrary of webpack typically). It compiles each *.ts file individually, and outputs corresponding *.js files representing each of them, without touching the import paths.

    Hence your import * as L from 'leaflet'; line is copied as-is into the generated JS file.

    When you load that generated JS file with <script src="scripts/maps/property-map.js" type="module"></script>, the browser understands the import syntax (thanks to the module type), but there the module specifier needs to be different from your TypeScript project:

    Note: In some module systems, you can omit the file extension and the leading /, ./, or ../ (e.g. 'modules/square'). This doesn't work in native JavaScript modules.

    Hence your error message.

    You can try these 2 possible solutions:

    1. Get rid of the import
    2. Use ES module absolute import path as an URL

    1. Get rid of the import

    Assuming your scripts/maps/leaflet.js file is the actual Leaflet library script in UMD form (so that it provides the global L object) (either the source or minified version), then you actually do not need to import it explicitly:

    By default all visible@types” packages are included in your compilation. Packages in node_modules/@types of any enclosing folder are considered visible.

    • In the browser loading the generated JS file without the import, it knows what L is, because it is previously assigned by Leaflet library as UMD

    Make sure to add "allowUmdGlobalAccess": true to your compilerOptions in tsconfig.json

    2. Use ES module absolute import path as an URL

    If you prefer sticking to explicit import, then you could still load Leaflet with an absolute path, but to be compatible with the browser native JS modules, absolute paths are actually URL's:

    import * as L from "https://cdn.jsdelivr.net/npm/[email protected]/dist/leaflet-src.esm.js";
    

    Again, the TS compiler will copy that path untouched, and the browser will now be happy (and you can even get rid of the <script src="scripts/maps/leaflet.js" type="module"></script> tag in your index.html file; the latter will automatically load Leaflet from the specified URL, as intended from JS modules!)

    However, your TS project, on the contrary, will no longer understand that path, and therefore no longer know what L is.

    To make it happy again, we can tell TypeScript what that import path is in terms of types, using the paths alias in tsconfig.json:

    A series of entries which re-map imports to lookup locations relative to the baseUrl.

    {
      "compilerOptions": {
        // ...
        "baseUrl": ".", // Required for paths
        "paths": {
          "https://cdn.jsdelivr.net/npm/[email protected]/dist/leaflet-src.esm.js": [
            "node_modules/@types/leaflet/index" // Re-map to the type
          ]
        }
      },
      // ...
    }
    

    With this, we get the "best of both worlds": type safety, and native browser ES modules (hence no need for a bundler like webpack)