node.js - Hvordan får du '@/'-aliaset til å fungere i et Typescript ESM-prosjekt?


Jeg prøver å kjøre skriptet mitt ved å bruke ESM:

ts-node --esm -r tsconfig-paths/register -T src/server/api/jobs/index.ts

Men det ser ikke ut til å kunne håndtere importene mine som import '@/server/init.ts:

CustomError: Cannot find package '@/server' imported from /Users/me/Sites/myapp/src/server/api/jobs/index.ts

Jeg har prøvd å legge dette til i min tsconfig.json:

    "paths": {
      "@/*": ["src/*"]
    },

Jeg har også prøvd å endre «name» i min `package.json`:

"name": "@/server",

Min mappestruktur er:

/myapp
  package.json
  tsconfig.json
  /src
    /server
      /api
        /jobs
          index.ts

Her er min tsconfig.json:

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "allowImportingTsExtensions": true,
    "allowJs": true,
    "baseUrl": "./src",
    "checkJs": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "incremental": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "lib": ["dom", "dom.iterable", "ES5"],
    "module": "ESNext",
    "moduleResolution": "NodeNext",
    "noEmit": true,
    "noUncheckedIndexedAccess": true,
    "pretty": true,
    "resolveJsonModule": true,
    "rootDir": ".",
    "skipLibCheck": true,
    "strict": true,
    "target": "ESNext",
    "types": ["node"],
    "paths": {
      "@/*": ["src/*"]
    },
    "plugins": [
      {
        "name": "next"
      }
    ]
  },
  "include": [
    ".eslintrc.cjs",
    "next-env.d.ts",
    "**/*.ts",
    "*.js",
    "**/*.js",
    "**/*.tsx",
    "**/*.cjs",
    "**/*.mjs",
    ".next/types/**/*.ts",
    "./src/*",
    "src/*"
  ],
  "exclude": ["node_modules"]
  // "ts-node": {
  //   "esm": true,
  //   "experimentalSpecifierResolution": "node",
  //   "require": ["tsconfig-paths/register"]
  // }
}

Svar #1

Ts-node virker problematisk med ESM.

For mitt ESM-prosjekt brukte jeg det ikke, men tsx. La oss først gå gjennom hvordan jeg kjører TS-koden (i utvikling):

npm i --save-dev tsx

"main": "build/index.js",
"type": "module",
"scripts": {
    ...
    "dev": "tsx watch ./src/index.ts",
    ...

Over først satte jeg hovedfilen i bygge-mappen, hvor vi bygger produksjonskoden. Dette kan variere... dist/build/etc... Bruker modul for ESM. Deretter med tsx kjører vi med hot reload (watch) index.ts inngangsfilen rett under src.

Forresten, her er min tsconfig:

{
    "$schema": "https://json.schemastore.org/tsconfig",
    "display": "Node 20",
    "compilerOptions": {
        /* Specify what module code is generated. */
        "module": "NodeNext",
        /* Specify which ECMAScript version the project's output JavaScript code must support.*/
        "target": "ESNext",
        /*
         * Specify how TypeScript looks up a file from a given module specifier.
         * Bundler removes the need to specify the full path inside imports
         */
        "moduleResolution": "NodeNext",
        /* Skip type checking of declaration files. */
        "skipLibCheck": true,
        /* Removes comments from the project's output JavaScript code. */
        "removeComments": true,
        /* Enables experimental support for emitting type metadata for decorators which works with the module reflect-metadata. */
        "emitDecoratorMetadata": true,
        /* Enables experimental support for decorators, which is a version of decorators that predates the TC39 standardization process. */
        "experimentalDecorators": true,
        /* When set, TypeScript will include the original content of the .ts file as an embedded string in the source map (using the source map’s sourcesContent property). This is often useful in the same cases as inlineSourceMap. */
        "inlineSources": true,
        /* Enables the generation of sourcemap files. */
        "sourceMap": true,
        /* Allow JavaScript files to be imported inside the project, instead of just .ts and .tsx files. */
        "allowJs": true,
        /* By default (with esModuleInterop false or not set) TypeScript treats CommonJS/AMD/UMD modules similar to ES6 modules. */
        "esModuleInterop": true,
        /* The strict flag enables a wide range of type checking behavior that results in stronger guarantees of program correctness. */
        "strict": true,
        /* When set to true, TypeScript will raise an error when a class property was declared but not set in the constructor. */
        "strictPropertyInitialization": false,
        /* This flag controls how import works */
        "importsNotUsedAsValues": "remove",
        /* Specify the location where a debugger should locate TypeScript files instead of relative source locations */
        "sourceRoot": "/",
        /* The longest common path of all non-declaration input files. */
        "rootDir": "src",
        /* Directory name of the project's output JavaScript code. */
        "outDir": "build",
        /* Specify to generate .d.ts files for every TypeScript or JavaScript file inside the project. */
        "declaration": true,
        /* Sets a base directory from which to resolve non-relative module names. */
        "baseUrl": ".",
        /* A series of entries which re-map imports to lookup locations relative to the baseUrl if set, or to the tsconfig file itself otherwise. */
        "paths": {
            "@/*": ["./src/*"]
        },
    },
    "exclude": ["coverage", "build"]
}

Dette samsvarer med dine spesifikasjoner for å kjøre TS-kode fra src.

Hvis du har til hensikt å bygge kode til JS inn i (for nå la oss si) build-mappen, og kjøre det i produksjon:

Først må vi sette opp en patch til tsconfig.json, la oss kalle den for eksempel tsconfig.prod.json:

{
    "extends": "./tsconfig.json",
    "compilerOptions": {
        "paths": {
            "@/*": ["./src/*"]
        },
    }
}

Da må vi bygge denne koden med tsc:

"scripts": {
    ...
    "build": "rm -fr ./build && tsc",
    ....

Dette fjerner build-mappen hvis den finnes, og bygger den deretter på nytt med tsc.

La oss nå kjøre den bygde koden:

"scripts": {
    ...
    "start": "TS_NODE_PROJECT=./tsconfig.prod.json node .",
    ....

Vennligst husk at med disse banealiasene må du avslutte TS-referansene dine med .js-endelse. Det er ikke et teknisk problem, men etter min personlige mening ser det ekstremt stygt ut. Jeg har lidd de siste dagene for å finne ut en omfattende banealias-policy for prosjektet mitt, og få det til å kjøre på både utvikling og produksjon... Vennligst gi meg beskjed om hva du synes.

Svar #2

Jeg møtte de samme problemene, og støtter svaret fra @ernestdolog

Kontekst

Jeg bygger en REST API ved å bruke Fastify og bruker Typescript for koden i stedet for Javascript. Fordi dette ikke er et frontend-prosjekt (vite) eller et fullstack-prosjekt som NextJS, stolte jeg på ts-node for at koden min skulle bygges og kjøre.

Problemet oppsto ved installasjon av t3-omgivelsen, som er en kun-ESM-pakke, og derfor ikke kan brukes utenfor et TS-prosjekt som ikke er av typen module.

Når jeg gjorde dette, fungerte ingenting.

  • Jeg prøvde å utvide ts-node
  • Jeg prøvde å bruke nodemon med en tilpasset node-loader (se andre innlegg på nettet for dette)

Ingenting fungerte :

Løsningen

Jeg løste problemet ved å bruke tsx watch for utvikling og tsc for å bygge koden for produksjon.

Her er min package.json som viser hvordan build, start og dev-modus kjøres

{
  "name": "spendaro-api",
  "type": "module",
  "version": "1.0.0",
  "description": "Official Spendaro Budgeting App API Server",
  "main": "index.js",
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "dev": "tsx watch src/index.ts",
    "start": "node dist/index.js",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "db:generate-migration": "drizzle-kit generate",
    "db:migrate": "tsx src/db/migrate.ts",
    "db:seed": "src/db/seed.ts"
  },
  "dependencies": {
    "@fastify/cors": "^10.0.1",
    "@fastify/jwt": "^9.0.1",
    "@fastify/swagger": "^9.1.0",
    "@scalar/fastify-api-reference": "^1.25.27",
    "@scalar/themes": "^0.9.33",
    "@scalar/types": "^0.0.12",
    "@t3-oss/env-core": "^0.11.1",
    "@types/bcrypt": "^5.0.2",
    "bcrypt": "^5.1.1",
    "config": "^3.3.12",
    "drizzle-orm": "^0.33.0",
    "drizzle-zod": "^0.5.1",
    "fastify": "^5.0.0",
    "fastify-api-reference": "link:@types/@scalar/fastify-api-reference",
    "fastify-plugin": "^5.0.1",
    "http-status-codes": "^2.3.0",
    "postgres": "^3.4.4",
    "tsconfig-paths": "^4.2.0",
    "vite-node": "^2.1.2",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@antfu/eslint-config": "^3.7.3",
    "@tsconfig/node20": "^20.1.4",
    "@types/config": "^3.3.5",
    "@types/node": "^22.7.4",
    "drizzle-kit": "^0.24.2",
    "eslint": "^9.12.0",
    "eslint-plugin-format": "^0.1.2",
    "nodemon": "^3.1.7",
    "ts-node": "^10.9.2",
    "ts-node-dev": "^2.0.0",
    "tsx": "^4.19.1",
    "typescript": "^5.6.2"
  }
}

Og her er min tsconfig.json

{
  "compilerOptions": {
    "target": "ESNext",
    "baseUrl": ".",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "resolveJsonModule": true,
    "strict": true,
    "declaration": true,
    "noEmit": false,
    "outDir": "./dist",
    "removeComments": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

Hvis du vil referere til kodebasen og se hvordan ESM-importene med de stygge .js-utvidelsene ser ut og fungerer, her er det.

Her er et raskt eksempel på hvordan index.ts-inngangsfilen ser ut. Merk .ts-filtypen og bruken av .js-filtypeimporter. Dette er fortsatt nødvendig per oktober 2024.

import db from "./db/index.js";
import { ErrorHandlers } from "./handlers/error-handlers.js";
import { swaggerConfig } from "./open-api.js";
import authenticate from "./plugins/authenticate.js";
import { routes } from "./routes/index.js";
import { ALLOWED_METHODS } from "./utils/http.js";

const server = fastify({
  logger: {
    enabled: true,
    level: config.get("server.logging.level"),
  },
});

Relaterte artikler