modules icon indicating copy to clipboard operation
modules copied to clipboard

Node 13.9.0 drop support for extensionless npm scripts ("bin")

Open sla100 opened this issue 4 years ago • 12 comments

package.json:

{
  "scripts": {
    "test": "tsc"
  },
  "dependencies": {
    "typescript": "^3.7.5"
  }
}

.npmrc:

node-options="--loader=./loader.js"

loader.js:

// Custom loader. May be empty for test

Command:

npm test

Result:

Node 13.8.0:

tsc

Node 13.9.0:

tsc
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension "" for c:\workspace\node_13.9.0\node_modules\typescript\bin\tsc
    at Loader.defaultGetFormat [as _getFormat] (internal/modules/esm/get_format.js:70:15)

Why ? Becacuse a lot of tools has bin specification without scripts extension: ex.

  "bin": {
    "tsc": "bin/tsc",
    "tsserver": "bin/tsserver"
  },

Why did you introduce a change that blocks npm scripts?

sla100 avatar Feb 20 '20 19:02 sla100

Can you run node --version locally. There was never a Node.js version 3, as version three was part of the IO.js project. Currently supported versions of Node.js are 10, 12, 13.

Are you running code as CommonJS or ESM? If you are using ESM you need to include the full path to the resource you are trying to run.

There is nothing that we have done in Node.js to block npm scripts, which still are actively used across the ecosystem, and if I recall even internally in Node.js core for some of our tooling.

MylesBorins avatar Feb 20 '20 19:02 MylesBorins

Can you run node --version locally. There was never a Node.js version 3, as version three was part of the IO.js project. Currently supported versions of Node.js are 10, 12, 13.

$ node --version
v13.9.0

Are you running code as CommonJS or ESM? If you are using ESM you need to include the full path to the resource you are trying to run.

I'm not running "code" but npm script which found bin definded in ./node_modules/typescript/package.json file.

There is nothing that we have done in Node.js to block npm scripts, which still are actively used across the ecosystem, and if I recall even internally in Node.js core for some of our tooling.

There is a "broken" change: https://github.com/nodejs/node/pull/31415

sla100 avatar Feb 20 '20 19:02 sla100

This is because --loader forces the ESM resolution algorithm not CJS resolution, and extension-less files are not resolved by default for ESM, see https://github.com/nodejs/node/pull/31415

bmeck avatar Feb 20 '20 19:02 bmeck

Overall, this is the intended behavior we got to after problems with having support for files lacking extensions in the ESM resolver; so I wouldn't think we need to fix this. My recommendation is you can have tsc map to a file with an extension, or expand the custom --loader supplied to support loading files without extensions (since it is overwriting how tsc works anyway).

bmeck avatar Feb 20 '20 19:02 bmeck

You should be able to work around this by implementing your custom loader to support a get_format hook which allows extensionless files.

Something like:

import { extname } from 'path';
export async function getFormat(url, context, defaultGetFormat) {
  if (!extname(url.pathname)) {
    return {
      format: 'commonjs'
    };
  }
  return defaultGetFormat(url, context, defaultGetFormat);
}

guybedford avatar Feb 20 '20 19:02 guybedford

Note it would still be an option for us to allow files without an extension for the Node.js CLI entry point only, as we had previously, which was specially implemented before for backwards compatibility cases like this.

guybedford avatar Feb 20 '20 19:02 guybedford

You should be able to work around this by implementing your custom loader to support a get_format hook which allows extensionless files.

Thanks !. It's a duct-tape but it works!

sla100 avatar Feb 20 '20 19:02 sla100

@guybedford backwards compatibility breakage came from --loader, without --loader it still worked fine

bmeck avatar Feb 20 '20 19:02 bmeck

I read the Node documentation to confirm my ignorance of the loader (https://nodejs.org/dist/latest-v13.x/docs/api/esm.html#esm_experimental_loaders).

When hooks are used they only apply to ES module loading and not to any CommonJS modules loaded.

Again, I feel that this is a mistake becasuse defaultGetFormat incorrectly recognized Typescript module as esm when in reality he is cjs.

sla100 avatar Feb 20 '20 20:02 sla100

@guybedford

Finally i added test for (also!) extensionless internal modules which defaultGetFormat recognize correctly:

export async function getFormat(urlString, context, defaultGetFormat) {
  // support for extensionless files in "bin" scripts
  if (/^file:\/\/\/.*\/bin\//.test(url) && !path.extname(url)) {
    return {
      format: 'commonjs',
    };
  }
  return defaultGetFormat(urlString, context, defaultGetFormat);
}

sla100 avatar Feb 21 '20 07:02 sla100

Fixed some typos in sla100's loader fix and added the import:

import path from 'path';

export async function getFormat(urlString, context, defaultGetFormat) {
  // support for extensionless files in "bin" scripts
  if (/^file:\/\/\/.*\/bin\//.test(urlString) && !path.extname(urlString)) {
    return {
      format: 'module',
    };
  }
  return defaultGetFormat(urlString, context, defaultGetFormat);
}

To use this, node --experimental-loader loader.mjs ... though this does (currently) result in ugly warning messages.

Is there a more transparent way to include this?

tadman avatar Mar 23 '21 13:03 tadman

To use this, node --experimental-loader loader.mjs ... though this does (currently) result in ugly warning messages.

@tadman it silences all warnings, but i've been using NODE_NO_WARNINGS=1

unicornware avatar Oct 01 '21 22:10 unicornware