4

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.

6
  • Is this a node module or local file? This Error normaly only occurres when you want to import a local js file Commented May 9, 2022 at 12:01
  • I guess you're missing "module": "es2020", or "module": "es6", in compilerOptions. See Specify what module code is generated.. Commented May 9, 2022 at 12:08
  • @DominikLovetinsky it's a local JS file which is generated automatically from a local typescript file. Commented May 9, 2022 at 12:15
  • @insertusernamehere Didn't work, unfortunately Commented May 9, 2022 at 12:16
  • Try to add ./ to as prefix, because local files need to accessed via relative path Commented May 9, 2022 at 14:30

1 Answer 1

9
+100

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)

3
  • 1
    This is excellent, thank you! I went for "Option 1: Get rid of the import". After doing that, I got an error telling me "L refers to a UMD global but the current file is a module". This is because I had changed whole file file to the form "export class PropertyMap....". I solved the final error by removing the export part. It works and I am pleased. Thanks! Commented May 13, 2022 at 15:47
  • 2
    Have you tried the allowUmdGlobalAccess option?
    – ghybs
    Commented May 13, 2022 at 17:04
  • You sir are the gift that keeps on giving. Many thanks! Commented May 14, 2022 at 1:35

Not the answer you're looking for? Browse other questions tagged or ask your own question.