Skip to content
This repository has been archived by the owner on Feb 8, 2024. It is now read-only.

Should this work with type: module packages? #8

Open
vjpr opened this issue Aug 24, 2021 · 3 comments
Open

Should this work with type: module packages? #8

vjpr opened this issue Aug 24, 2021 · 3 comments

Comments

@vjpr
Copy link

vjpr commented Aug 24, 2021

When I try to import a .ts file from an ESM entry-point I get this error:

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /xxx/src/cli.ts
    at new NodeError (node:internal/errors:371:5)
    at Loader.defaultGetFormat [as _getFormat] (node:internal/modules/esm/get_format:71:15)
    at Loader.getFormat (node:internal/modules/esm/loader:105:42)
    at Loader.getModuleJob (node:internal/modules/esm/loader:243:31)
    at async Loader.import (node:internal/modules/esm/loader:177:17)
    at async file:///xxx/index.js:5:15 {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}

I think the underlying pirates hook only works with commonjs entry-points.

Right now ESM loader hooks are experimental and expected to change. See: nodejs/modules#351). Problem is you have to then run the entry-point using cross-env 'NODE_OPTIONS=--experimental-loader some-swc-hook' which is annoying.

Here is an example of a transpiler hook: https://nodejs.org/api/esm.html#esm_transpiler_loader

Here is how instanbuljs is doing it: https://github.com/istanbuljs/esm-loader-hook/blob/master/index.js

@vjpr
Copy link
Author

vjpr commented Aug 24, 2021

Workaround

Use a loader using the experimental ESM loader API.

cross-env NODE_OPTIONS='--experimental-loader my-loader' npm run dev

Can also use env-cmd to avoid having to specify the env for every npm run script, but be careful of toddbluhm/env-cmd#127.

// Adapted from: https://nodejs.org/api/esm.html#esm_transpiler_loader

import {URL, pathToFileURL} from 'url'
import {cwd} from 'process'
import * as swc from '@swc/core'
import sourceMapSupport from 'source-map-support'
import path, {join} from 'path'

const baseURL = pathToFileURL(`${cwd()}/`).href

const extensionsRegex = /\.ts$/

////////////////////////////////////////////////////////////////////////////////

//
// `specifier` is like `request` using CJS terminology.
//    E.g.
//    - file:///xxx/cli-swc/bin/index.js
//    - regenerator-runtime
//    - @live/simple-cli-helper/lib/swc.js
//    - ./index
//
export function resolve(specifier, context, defaultResolve) {
  const {parentURL = baseURL} = context

  // Node.js `defaultResolve` normally errors on unknown file extensions so we resolve it ourselves.

  if (extensionsRegex.test(specifier)) {
    const newUrl = new URL(specifier, parentURL)
    return {url: newUrl.href}
  }

  if (specifier.startsWith('./') && !hasExtension(specifier)) {
    // If no extension, assume TS.
    const newUrl = new URL(specifier, parentURL)
    newUrl.pathname += '.ts'
    return {url: newUrl.href}
  }

  // Let Node.js handle all other specifiers.
  return defaultResolve(specifier, context, defaultResolve)
}

function hasExtension(specifier) {
  return path.extname(specifier) !== ''
}

export function getFormat(url, context, defaultGetFormat) {
  // Now that we patched resolve to let TS URLs through, we need to
  // tell Node.js what format such URLs should be interpreted as. For the
  // purposes of this loader, all TS URLs are ES modules.
  if (extensionsRegex.test(url)) {
    return {
      format: 'module',
    }
  }

  // Let Node.js handle all other URLs.
  return defaultGetFormat(url, context, defaultGetFormat)
}

export function transformSource(source, context, defaultTransformSource) {
  const {url, format} = context

  const opts = {}

  if (extensionsRegex.test(url)) {
    if (typeof source !== 'string') {
      source = source.toString()
    }
    const code = compile(url, source, opts)
    return {source: code}
  }

  // Let Node.js handle all other sources.
  return defaultTransformSource(source, context, defaultTransformSource)
}

////////////////////////////////////////////////////////////////////////////////

const maps: {[src: string]: string} = {}

function compile(filename, code, opts) {
  const output: swc.Output = swc.transformSync(code, {
    ...opts,
    sourceMaps: opts.sourceMaps === undefined ? 'inline' : opts.sourceMaps,
  })
  if (output.map) {
    if (Object.keys(maps).length === 0) {
      installSourceMapSupport()
    }
    maps[filename] = output.map
  }
  return output.code
}

////////////////////////////////////////////////////////////////////////////////

function installSourceMapSupport() {
  sourceMapSupport.install({
    handleUncaughtExceptions: false,
    environment: 'node',
    retrieveSourceMap(source) {
      const map = maps && maps[source]
      if (map) {
        return {
          url: null as any,
          map: map,
        }
      } else {
        return null
      }
    },
  })
}

@kdy1
Copy link
Member

kdy1 commented Aug 24, 2021

I think it should work, but I don't think it's okay to drop support for old js packages.

@athenacfr
Copy link

I think it should work, but I don't think it's okay to drop support for old js packages.

I don't think the idea is to drop support for old js packages, but add support for new ones. Something like node --loader @swc-node/loader src/main.ts

Also, loader flag is not experimental anymore. I think we should rethink about this since esm modules are getting more popular.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

No branches or pull requests

3 participants