diff --git a/doc/api/errors.md b/doc/api/errors.md
index 61ba0d9e4a91d4..02af1e50afd210 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -3041,7 +3041,8 @@ import 'package-name'; // supported
added: v22.6.0
-->
-Type stripping is not supported for files descendent of a `node_modules` directory.
+Type stripping is not supported for files descendent of a `node_modules` directory,
+where package.json is missing or does not contain the property `"private": true`.
diff --git a/doc/api/typescript.md b/doc/api/typescript.md
index bad8f44001c04e..0f376f97831bdc 100644
--- a/doc/api/typescript.md
+++ b/doc/api/typescript.md
@@ -155,7 +155,7 @@ are enabled by default.
To discourage package authors from publishing packages written in TypeScript,
Node.js will by default refuse to handle TypeScript files inside folders under
-a `node_modules` path.
+a `node_modules` path, unless the package.json contains the property `"private": true`.
[CommonJS]: modules.md
[ES Modules]: esm.md
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 8a97384201341f..696e0c48f57f63 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1839,7 +1839,8 @@ E('ERR_UNSUPPORTED_ESM_URL_SCHEME', (url, supported) => {
return msg;
}, Error);
E('ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING',
- 'Stripping types is currently unsupported for files under node_modules, for "%s"',
+ 'Stripping types is currently unsupported for files under node_modules that ' +
+ 'do not contain a package.json with the property private: true, 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.',
diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js
index ef94dd768fbd52..013f2795fe9579 100644
--- a/lib/internal/modules/cjs/loader.js
+++ b/lib/internal/modules/cjs/loader.js
@@ -124,7 +124,6 @@ const { pathToFileURL, fileURLToPath, isURL } = require('internal/url');
const {
pendingDeprecate,
emitExperimentalWarning,
- isUnderNodeModules,
kEmptyObject,
setOwnProperty,
getLazy,
@@ -170,7 +169,6 @@ const {
ERR_REQUIRE_CYCLE_MODULE,
ERR_REQUIRE_ESM,
ERR_UNKNOWN_BUILTIN_MODULE,
- ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
},
setArrowMessage,
} = require('internal/errors');
@@ -1370,9 +1368,6 @@ let hasPausedEntry = false;
function loadESMFromCJS(mod, filename) {
let source = getMaybeCachedSource(mod, filename);
if (getOptionValue('--experimental-strip-types') && path.extname(filename) === '.mts') {
- if (isUnderNodeModules(filename)) {
- throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
- }
source = stripTypeScriptTypes(source, filename);
}
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
@@ -1594,9 +1589,6 @@ function getMaybeCachedSource(mod, filename) {
}
function loadCTS(module, filename) {
- if (isUnderNodeModules(filename)) {
- throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
- }
const source = getMaybeCachedSource(module, filename);
const code = stripTypeScriptTypes(source, filename);
module._compile(code, filename, 'commonjs');
@@ -1608,14 +1600,12 @@ function loadCTS(module, filename) {
* @param {string} filename The file path of the module
*/
function loadTS(module, filename) {
- if (isUnderNodeModules(filename)) {
- throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
- }
// If already analyzed the source, then it will be cached.
const source = getMaybeCachedSource(module, filename);
- const content = stripTypeScriptTypes(source, filename);
let format;
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
+ const isPrivatePackageJSON = pkg?.data.private;
+ const content = stripTypeScriptTypes(source, filename, isPrivatePackageJSON);
// Function require shouldn't be used in ES modules.
if (pkg?.data.type === 'module') {
if (getOptionValue('--experimental-require-module')) {
@@ -1633,7 +1623,7 @@ function loadTS(module, filename) {
if (Module._cache[parentPath]) {
let parentSource;
try {
- parentSource = stripTypeScriptTypes(fs.readFileSync(parentPath, 'utf8'), parentPath);
+ parentSource = stripTypeScriptTypes(fs.readFileSync(parentPath, 'utf8'), parentPath, isPrivatePackageJSON);
} catch {
// Continue regardless of error.
}
diff --git a/lib/internal/modules/esm/load.js b/lib/internal/modules/esm/load.js
index d56dae3f001b1c..5ba13096b98047 100644
--- a/lib/internal/modules/esm/load.js
+++ b/lib/internal/modules/esm/load.js
@@ -4,7 +4,6 @@ const {
RegExpPrototypeExec,
} = primordials;
const {
- isUnderNodeModules,
kEmptyObject,
} = require('internal/util');
@@ -23,7 +22,6 @@ const {
ERR_INVALID_URL,
ERR_UNKNOWN_MODULE_FORMAT,
ERR_UNSUPPORTED_ESM_URL_SCHEME,
- ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
} = require('internal/errors').codes;
const {
@@ -131,12 +129,6 @@ async function defaultLoad(url, context = kEmptyObject) {
validateAttributes(url, format, importAttributes);
- if (getOptionValue('--experimental-strip-types') &&
- (format === 'module-typescript' || format === 'commonjs-typescript') &&
- isUnderNodeModules(url)) {
- throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(url);
- }
-
return {
__proto__: null,
format,
diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js
index 172f0fdc02a686..2f7fcc6bfae39a 100644
--- a/lib/internal/modules/helpers.js
+++ b/lib/internal/modules/helpers.js
@@ -16,6 +16,7 @@ const {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_INVALID_TYPESCRIPT_SYNTAX,
+ ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
} = require('internal/errors').codes;
const { BuiltinModule } = require('internal/bootstrap/realm');
@@ -28,7 +29,7 @@ const assert = require('internal/assert');
const { Buffer } = require('buffer');
const { getOptionValue } = require('internal/options');
-const { assertTypeScript, setOwnProperty, getLazy } = require('internal/util');
+const { assertTypeScript, setOwnProperty, getLazy, isUnderNodeModules } = require('internal/util');
const { inspect } = require('internal/util/inspect');
const lazyTmpdir = getLazy(() => require('os').tmpdir());
@@ -347,6 +348,21 @@ function parseTypeScript(source, options) {
}
}
+/**
+ *
+ * @param {string} filename
+ * @returns {boolean} Returns true if the nearest package.json is private.
+ */
+function isPrivateNodeModule(filename) {
+ const packageJsonReader = require('internal/modules/package_json_reader');
+ const resolved = StringPrototypeStartsWith(filename, 'file://') ? filename : pathToFileURL(filename).href;
+ const packageConfig = packageJsonReader.getPackageScopeConfig(resolved);
+ if (!packageConfig?.exists) {
+ return false;
+ }
+ return packageConfig.isPrivate === true;
+}
+
/**
* @typedef {object} TransformOutput
* @property {string} code The compiled code.
@@ -355,9 +371,16 @@ function parseTypeScript(source, options) {
* Performs type-stripping to TypeScript source code.
* @param {string} source TypeScript code to parse.
* @param {string} filename The filename of the source code.
+ * @param {string} isPackageJsonPrivate Whether the nearest package.json is private.
* @returns {TransformOutput} The stripped TypeScript code.
*/
-function stripTypeScriptTypes(source, filename) {
+function stripTypeScriptTypes(source, filename, isPackageJsonPrivate = false) {
+ // If the package.json is private, we can
+ // allow the stripping of types because it is
+ // not possible to publish it on npm.
+ if (isUnderNodeModules(filename) && isPackageJsonPrivate !== true && !isPrivateNodeModule(filename)) {
+ throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
+ }
assert(typeof source === 'string');
const options = {
__proto__: null,
diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js
index 9a9dcebb799c00..5225a5c7a4f707 100644
--- a/lib/internal/modules/package_json_reader.js
+++ b/lib/internal/modules/package_json_reader.js
@@ -34,6 +34,7 @@ function deserializePackageJSON(path, contents) {
3: plainImports,
4: plainExports,
5: optionalFilePath,
+ 6: isPrivate,
} = contents;
// This is required to be used in getPackageScopeConfig.
@@ -64,6 +65,7 @@ function deserializePackageJSON(path, contents) {
ObjectDefineProperty(this, 'exports', { __proto__: null, value });
return this.exports;
},
+ isPrivate,
};
}
diff --git a/src/node_modules.cc b/src/node_modules.cc
index dfd115a9eccc6b..456c9f6f379f38 100644
--- a/src/node_modules.cc
+++ b/src/node_modules.cc
@@ -20,6 +20,7 @@ namespace node {
namespace modules {
using v8::Array;
+using v8::Boolean;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::HandleScope;
@@ -74,15 +75,16 @@ Local BindingData::PackageConfig::Serialize(Realm* realm) const {
isolate, input.data(), NewStringType::kNormal, input.size())
.ToLocalChecked();
};
- Local values[6] = {
+ Local values[7] = {
name.has_value() ? ToString(*name) : Undefined(isolate),
main.has_value() ? ToString(*main) : Undefined(isolate),
ToString(type),
imports.has_value() ? ToString(*imports) : Undefined(isolate),
exports.has_value() ? ToString(*exports) : Undefined(isolate),
ToString(file_path),
+ Boolean::New(isolate, is_private),
};
- return Array::New(isolate, values, 6);
+ return Array::New(isolate, values, 7);
}
const BindingData::PackageConfig* BindingData::GetPackageJSON(
@@ -225,6 +227,10 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON(
default:
break;
}
+ } else if (key == "private") {
+ if (value.get_bool().get(package_config.is_private)) {
+ return throw_invalid_package_config();
+ }
}
}
// package_config could be quite large, so we should move it instead of
diff --git a/src/node_modules.h b/src/node_modules.h
index 17909b2270454b..34bbc4425adc07 100644
--- a/src/node_modules.h
+++ b/src/node_modules.h
@@ -34,6 +34,7 @@ class BindingData : public SnapshotableObject {
std::optional imports;
std::optional scripts;
std::string raw_json;
+ bool is_private;
v8::Local Serialize(Realm* realm) const;
};
diff --git a/test/es-module/test-typescript.mjs b/test/es-module/test-typescript.mjs
index a7ca6d70dd5e10..68ac23f80357c1 100644
--- a/test/es-module/test-typescript.mjs
+++ b/test/es-module/test-typescript.mjs
@@ -414,3 +414,15 @@ test('expect error when executing a TypeScript file with generics', async () =>
strictEqual(result.stdout, '');
strictEqual(result.code, 1);
});
+
+test('execute TypeScript file from node_modules with private: true', async () => {
+ const result = await spawnPromisified(process.execPath, [
+ '--experimental-strip-types',
+ '--no-warnings',
+ fixtures.path('typescript/ts/test-import-private-ts-node-module.ts'),
+ ]);
+
+ strictEqual(result.stderr, '');
+ match(result.stdout, /Hello, TypeScript!/);
+ strictEqual(result.code, 0);
+});
diff --git a/test/fixtures/typescript/ts/node_modules/bar/package.json b/test/fixtures/typescript/ts/node_modules/bar/package.json
index ff1ab7524e4743..1ecd5a3cdb5b47 100644
--- a/test/fixtures/typescript/ts/node_modules/bar/package.json
+++ b/test/fixtures/typescript/ts/node_modules/bar/package.json
@@ -2,6 +2,7 @@
"name": "bar",
"version": "1.0.0",
"main": "bar.ts",
+ "private": false,
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
diff --git a/test/fixtures/typescript/ts/node_modules/private/package.json b/test/fixtures/typescript/ts/node_modules/private/package.json
new file mode 100644
index 00000000000000..d66386a10d9247
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/private/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "private",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "main": "private.ts",
+ "devDependencies": {},
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "description": ""
+}
diff --git a/test/fixtures/typescript/ts/node_modules/private/private.ts b/test/fixtures/typescript/ts/node_modules/private/private.ts
new file mode 100644
index 00000000000000..dfa706d4009a2b
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/private/private.ts
@@ -0,0 +1 @@
+export const foo: string = "Hello, TypeScript!"
diff --git a/test/fixtures/typescript/ts/test-import-private-ts-node-module.ts b/test/fixtures/typescript/ts/test-import-private-ts-node-module.ts
new file mode 100644
index 00000000000000..087bd29a092318
--- /dev/null
+++ b/test/fixtures/typescript/ts/test-import-private-ts-node-module.ts
@@ -0,0 +1,5 @@
+import { foo } from 'private';
+
+interface Bar { };
+
+console.log(foo);
diff --git a/typings/internalBinding/modules.d.ts b/typings/internalBinding/modules.d.ts
index 8142874edfde88..80c63bedbbc0ef 100644
--- a/typings/internalBinding/modules.d.ts
+++ b/typings/internalBinding/modules.d.ts
@@ -6,7 +6,8 @@ export type PackageConfig = {
main?: any
type: PackageType
exports?: string | string[] | Record
- imports?: string | string[] | Record
+ imports?: string | string[] | Record,
+ isPrivate?: boolean
}
export type SerializedPackageConfig = [
PackageConfig['name'],
@@ -15,6 +16,7 @@ export type SerializedPackageConfig = [
string | undefined, // exports
string | undefined, // imports
string | undefined, // raw json available for experimental policy
+ boolean | undefined // isPrivate
]
export interface ModulesBinding {