register
register copied to clipboard
Should this work with `type: module` packages?
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: https://github.com/nodejs/modules/issues/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
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 https://github.com/toddbluhm/env-cmd/issues/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
}
},
})
}
I think it should work, but I don't think it's okay to drop support for old js packages.
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.