diff --git a/doc/api/errors.md b/doc/api/errors.md index 0649fa25babe25..7e89f10a47b90a 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -3314,16 +3314,6 @@ import 'package-name'; // supported `import` with URL schemes other than `file` and `data` is unsupported. - - -### `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` - - - -Type stripping is not supported for files descendent of a `node_modules` directory. - ### `ERR_UNSUPPORTED_RESOLVE_REQUEST` @@ -4190,6 +4180,19 @@ An attempt was made to launch a Node.js process with an unknown `stdout` or `stderr` file type. This error is usually an indication of a bug within Node.js itself, although it is possible for user code to trigger it. + + +### `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` + + + +Type stripping was not supported for files descendent of a `node_modules` +directory. This restriction has been removed, and these files are now +type-stripped like any other TypeScript file. + ### `ERR_V8BREAKITERATOR` diff --git a/doc/api/typescript.md b/doc/api/typescript.md index 3959fd58b56c84..0d1f25598fb58c 100644 --- a/doc/api/typescript.md +++ b/doc/api/typescript.md @@ -2,6 +2,10 @@ + +Type stripping works for TypeScript files inside folders under a +`node_modules` path, so packages can ship TypeScript sources directly +instead of transpiling to JavaScript before publishing. + +Like all type-stripped files, files in `node_modules` are limited to +erasable TypeScript syntax and follow the same rules as the rest of the +application code; for example, [file extensions are mandatory][] in `import` +specifiers. Package authors who want to publish TypeScript sources should +reference the `.ts` files directly from the [`"exports"`][] field of +`package.json`. + +To avoid paying the cost of stripping types from dependencies on every +startup, the [module compile cache][] can be used to cache the stripped +output on disk. ### Paths aliases @@ -226,6 +249,7 @@ with `#`. [CommonJS]: modules.md [ES Modules]: esm.md [Full TypeScript support]: #full-typescript-support +[`"exports"`]: packages.md#exports [`--no-strip-types`]: cli.md#--no-strip-types [`ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`]: errors.md#err_unsupported_typescript_syntax [`tsconfig` "paths"]: https://www.typescriptlang.org/tsconfig/#paths @@ -233,6 +257,7 @@ with `#`. [`verbatimModuleSyntax`]: https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax [file extensions are mandatory]: esm.md#mandatory-file-extensions [full support]: #full-typescript-support +[module compile cache]: module.md#module-compile-cache [subpath imports]: packages.md#subpath-imports [the same way as `.js` files.]: packages.md#determining-module-system [type stripping]: #type-stripping diff --git a/lib/internal/errors.js b/lib/internal/errors.js index cc8ad3db3540f1..f2d02220d55f3c 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1898,9 +1898,6 @@ E('ERR_UNSUPPORTED_ESM_URL_SCHEME', (url, supported) => { msg += `. Received protocol '${url.protocol}'`; return msg; }, Error); -E('ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING', - 'Stripping types is currently unsupported for files under node_modules, for "%s"', - Error); E('ERR_UNSUPPORTED_RESOLVE_REQUEST', 'Failed to resolve module specifier "%s" from "%s": Invalid relative URL or base scheme is not hierarchical.', TypeError); diff --git a/lib/internal/modules/typescript.js b/lib/internal/modules/typescript.js index 43bc57d977ec27..b68d98e951ee51 100644 --- a/lib/internal/modules/typescript.js +++ b/lib/internal/modules/typescript.js @@ -11,11 +11,9 @@ const { const { assertTypeScript, emitExperimentalWarning, getLazy, - isUnderNodeModules, kEmptyObject } = require('internal/util'); const { ERR_INVALID_TYPESCRIPT_SYNTAX, - ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING, ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, } = require('internal/errors').codes; const assert = require('internal/assert'); @@ -151,9 +149,6 @@ function stripTypeScriptTypesForCoverage(code) { */ function stripTypeScriptModuleTypes(source, filename, sourceURL) { assert(typeof source === 'string'); - if (isUnderNodeModules(filename)) { - throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename); - } // Get a compile cache entry into the native compile cache store, // keyed by the filename. If the cache can already be loaded on disk, // cached.transpiled contains the cached string. Otherwise we should do diff --git a/test/es-module/test-typescript-commonjs.mjs b/test/es-module/test-typescript-commonjs.mjs index bcbd275d59554e..20fac1fb5c433d 100644 --- a/test/es-module/test-typescript-commonjs.mjs +++ b/test/es-module/test-typescript-commonjs.mjs @@ -133,24 +133,26 @@ test('execute a .cts file importing a .mts file export', async () => { assert.strictEqual(result.code, 0); }); -test('expect failure of a .cts file in node_modules', async () => { +test('execute a .cts file in node_modules', async () => { const result = await spawnPromisified(process.execPath, [ + '--no-warnings', fixtures.path('typescript/cts/test-cts-node_modules.cts'), ]); - assert.strictEqual(result.stdout, ''); - assert.match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/); - assert.strictEqual(result.code, 1); + assert.strictEqual(result.stderr, ''); + assert.match(result.stdout, /Hello, TypeScript!/); + assert.strictEqual(result.code, 0); }); -test('expect failure of a .ts file in node_modules', async () => { +test('execute a .ts file in node_modules', async () => { const result = await spawnPromisified(process.execPath, [ + '--no-warnings', fixtures.path('typescript/cts/test-ts-node_modules.cts'), ]); - assert.strictEqual(result.stdout, ''); - assert.match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/); - assert.strictEqual(result.code, 1); + assert.strictEqual(result.stderr, ''); + assert.match(result.stdout, /Hello, TypeScript!/); + assert.strictEqual(result.code, 0); }); test('expect failure of a .cts requiring esm without default type module', async () => { @@ -164,15 +166,16 @@ test('expect failure of a .cts requiring esm without default type module', async assert.strictEqual(result.code, 1); }); -test('expect failure of a .cts file requiring esm in node_modules', async () => { +test('execute a .cts file requiring esm in node_modules', async () => { const result = await spawnPromisified(process.execPath, [ '--experimental-require-module', + '--no-warnings', fixtures.path('typescript/cts/test-mts-node_modules.cts'), ]); - assert.strictEqual(result.stdout, ''); - assert.match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/); - assert.strictEqual(result.code, 1); + assert.strictEqual(result.stderr, ''); + assert.match(result.stdout, /Hello, TypeScript!/); + assert.strictEqual(result.code, 0); }); test('cts -> require mts -> import cts', async () => { diff --git a/test/es-module/test-typescript-module.mjs b/test/es-module/test-typescript-module.mjs index 010f42d825510b..d906c32439fae6 100644 --- a/test/es-module/test-typescript-module.mjs +++ b/test/es-module/test-typescript-module.mjs @@ -49,32 +49,35 @@ test('execute an .mts file importing a .cts file', async () => { test('execute an .mts file from node_modules', async () => { const result = await spawnPromisified(process.execPath, [ + '--no-warnings', fixtures.path('typescript/mts/test-mts-node_modules.mts'), ]); - assert.match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/); - assert.strictEqual(result.stdout, ''); - assert.strictEqual(result.code, 1); + assert.strictEqual(result.stderr, ''); + assert.match(result.stdout, /Hello, TypeScript!/); + assert.strictEqual(result.code, 0); }); test('execute a .cts file from node_modules', async () => { const result = await spawnPromisified(process.execPath, [ + '--no-warnings', fixtures.path('typescript/mts/test-cts-node_modules.mts'), ]); - assert.match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/); - assert.strictEqual(result.stdout, ''); - assert.strictEqual(result.code, 1); + assert.strictEqual(result.stderr, ''); + assert.match(result.stdout, /Hello, TypeScript!/); + assert.strictEqual(result.code, 0); }); test('execute a .ts file from node_modules', async () => { const result = await spawnPromisified(process.execPath, [ + '--no-warnings', fixtures.path('typescript/mts/test-ts-node_modules.mts'), ]); - assert.match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/); - assert.strictEqual(result.stdout, ''); - assert.strictEqual(result.code, 1); + assert.strictEqual(result.stderr, ''); + assert.match(result.stdout, /Hello, TypeScript!/); + assert.strictEqual(result.code, 0); }); test('execute an empty .ts file', async () => { diff --git a/test/es-module/test-typescript.mjs b/test/es-module/test-typescript.mjs index fd58d1d990a527..60946532bc3c58 100644 --- a/test/es-module/test-typescript.mjs +++ b/test/es-module/test-typescript.mjs @@ -61,6 +61,17 @@ test('execute a TypeScript file with node_modules', async () => { assert.strictEqual(result.code, 0); }); +test('execute a TypeScript package from node_modules via "exports"', async () => { + const result = await spawnPromisified(process.execPath, [ + '--no-warnings', + fixtures.path('typescript/ts/test-import-ts-exports-node-modules.ts'), + ]); + + assert.strictEqual(result.stderr, ''); + assert.match(result.stdout, /Hello, TypeScript!\nHello, TypeScript!/); + assert.strictEqual(result.code, 0); +}); + test('expect error when executing a TypeScript file with imports with no extensions', async () => { const result = await spawnPromisified(process.execPath, [ fixtures.path('typescript/ts/test-import-no-extension.ts'), @@ -166,12 +177,13 @@ test('expect stack trace of a TypeScript file to be correct', async () => { test('execute CommonJS TypeScript file from node_modules with require-module', async () => { const result = await spawnPromisified(process.execPath, [ + '--no-warnings', fixtures.path('typescript/ts/test-import-ts-node-modules.ts'), ]); - assert.match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/); - assert.strictEqual(result.stdout, ''); - assert.strictEqual(result.code, 1); + assert.strictEqual(result.stderr, ''); + assert.match(result.stdout, /Hello, TypeScript!/); + assert.strictEqual(result.code, 0); }); test('execute a TypeScript file with CommonJS syntax requiring .cts', async () => { diff --git a/test/fixtures/typescript/ts/node_modules/typescript-pkg/index.ts b/test/fixtures/typescript/ts/node_modules/typescript-pkg/index.ts new file mode 100644 index 00000000000000..e31a7e5b3b59f8 --- /dev/null +++ b/test/fixtures/typescript/ts/node_modules/typescript-pkg/index.ts @@ -0,0 +1,7 @@ +import { sub } from './lib/sub.mts'; + +export interface Greeting { + message: string; +} + +export const greeting: Greeting = { message: sub }; diff --git a/test/fixtures/typescript/ts/node_modules/typescript-pkg/lib/sub.mts b/test/fixtures/typescript/ts/node_modules/typescript-pkg/lib/sub.mts new file mode 100644 index 00000000000000..4b6380e84758b4 --- /dev/null +++ b/test/fixtures/typescript/ts/node_modules/typescript-pkg/lib/sub.mts @@ -0,0 +1 @@ +export const sub: string = 'Hello, TypeScript!'; diff --git a/test/fixtures/typescript/ts/node_modules/typescript-pkg/package.json b/test/fixtures/typescript/ts/node_modules/typescript-pkg/package.json new file mode 100644 index 00000000000000..f569b2e4c86ffa --- /dev/null +++ b/test/fixtures/typescript/ts/node_modules/typescript-pkg/package.json @@ -0,0 +1,9 @@ +{ + "name": "typescript-pkg", + "version": "1.0.0", + "type": "module", + "exports": { + ".": "./index.ts", + "./subpath": "./lib/sub.mts" + } +} diff --git a/test/fixtures/typescript/ts/test-import-ts-exports-node-modules.ts b/test/fixtures/typescript/ts/test-import-ts-exports-node-modules.ts new file mode 100644 index 00000000000000..09fb62d1bcb76e --- /dev/null +++ b/test/fixtures/typescript/ts/test-import-ts-exports-node-modules.ts @@ -0,0 +1,7 @@ +import { greeting } from 'typescript-pkg'; +import { sub } from 'typescript-pkg/subpath'; + +const message: string = greeting.message; + +console.log(message); +console.log(sub);