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);