Commit 2052d0dcdf7 for nodejs
commit 2052d0dcdf7f75e9d65618db8f7e60027ef94588
Author: Marco Ippolito <marcoippolito54@gmail.com>
Date: Mon Oct 19 15:03:51 2026 +0200
module: add --experimental-strip-private-modules
Signed-off-by: Marco Ippolito <marcoippolito54@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/63869
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com>
diff --git a/doc/api/cli.md b/doc/api/cli.md
index b326cfeb2b9..7230231decd 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -1647,6 +1647,25 @@ added:
Enable the experimental [`node:stream/iter`][] module.
+### `--experimental-strip-private-modules`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+By default, Node.js refuses to strip types from TypeScript files inside
+folders under a `node_modules` path. This flag enables type stripping for
+such files when they belong to a package whose `package.json` contains
+`"private": true`. Files belonging to non-private packages still throw
+[`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`][].
+The limitation to private packages should never be lifted,
+as publishing packages with TypeScript sources
+is highly discouraged.
+Stripping types inside `node_modules` is intended only
+for local development.
+
### `--experimental-test-coverage`
<!-- YAML
@@ -4287,6 +4306,7 @@ one is included in the list below.
* `--experimental-shadow-realm`
* `--experimental-specifier-resolution`
* `--experimental-stream-iter`
+* `--experimental-strip-private-modules`
* `--experimental-test-isolation`
* `--experimental-top-level-await`
* `--experimental-vm-modules`
@@ -4941,6 +4961,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man3.0/man3/CRYPTO_secure_malloc_init.html
[`ERR_INVALID_TYPESCRIPT_SYNTAX`]: errors.md#err_invalid_typescript_syntax
[`ERR_RX_MEMORY_NOT_SUPPORTED`]: errors.md#err_rx_memory_not_supported
+[`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`]: errors.md#err_unsupported_node_modules_type_stripping
[`ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`]: errors.md#err_unsupported_typescript_syntax
[`ERR_WEBASSEMBLY_NOT_SUPPORTED`]: errors.md#err_webassembly_not_supported
[`NODE_OPTIONS`]: #node_optionsoptions
diff --git a/doc/api/packages.md b/doc/api/packages.md
index 8e1f5038f96..f448cd68ba4 100644
--- a/doc/api/packages.md
+++ b/doc/api/packages.md
@@ -1153,6 +1153,9 @@ The following fields in `package.json` files are used in Node.js:
limits which submodules can be loaded from within the package.
* [`"imports"`][] - Package imports, for use by modules within the package
itself.
+* [`"private"`][] - When `true`, marks the package as not for publication. Used
+ by [`--experimental-strip-private-modules`][] to allow type stripping for the
+ package's TypeScript files under `node_modules`.
### `"name"`
@@ -1348,6 +1351,33 @@ Package imports permit mapping to external packages.
This field defines [subpath imports][] for the current package.
+### `"private"`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* Type: {boolean}
+
+```json
+{
+ "private": true
+}
+```
+
+The `"private"` field marks a package as private, signaling to package managers
+such as _npm_ that it must not be published to a public registry.
+By default, Node.js refuses to strip types from TypeScript files inside
+`node_modules`. When [`--experimental-strip-private-modules`][] is enabled,
+Node.js allows type stripping for:
+
+* files that are not inside a `node_modules` directory.
+* files inside `node_modules`, but only when the package that contains the
+ file sets `"private": true`. Only the `package.json` at each package boundary
+ is considered: the package's root directory, which is the directory directly
+ under `node_modules` (or under `node_modules/<@scope>` for scoped packages).
+ Nested `package.json` files are ignored.
+
[CommonJS]: modules.md
[Conditional exports]: #conditional-exports
[ES module]: esm.md
@@ -1362,10 +1392,12 @@ This field defines [subpath imports][] for the current package.
[`"imports"`]: #imports
[`"main"`]: #main
[`"name"`]: #name
+[`"private"`]: #private
[`"type"`]: #type
[`--conditions` / `-C` flag]: #resolving-user-conditions
[`--experimental-addon-modules`]: cli.md#--experimental-addon-modules
[`--experimental-package-map`]: cli.md#--experimental-package-mappath
+[`--experimental-strip-private-modules`]: cli.md#--experimental-strip-private-modules
[`--no-addons` flag]: cli.md#--no-addons
[`ERR_PACKAGE_MAP_EXTERNAL_FILE`]: errors.md#err_package_map_external_file
[`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported
diff --git a/doc/api/typescript.md b/doc/api/typescript.md
index 701280375ba..5ea7dea0202 100644
--- a/doc/api/typescript.md
+++ b/doc/api/typescript.md
@@ -222,6 +222,15 @@ To discourage package authors from publishing packages written in TypeScript,
Node.js refuses to handle TypeScript files inside folders under a `node_modules`
path.
+The [`--experimental-strip-private-modules`][] flag relaxes this restriction
+for packages whose `package.json` contains `"private": true`, such as
+workspace packages in a monorepo, which are not published to a registry.
+The limitation to private packages should never be lifted,
+as publishing packages with TypeScript sources
+is highly discouraged.
+Stripping types inside `node_modules` is intended only
+for local development.
+
### Paths aliases
[`tsconfig` "paths"][] won't be transformed and therefore produce an error. The closest
@@ -231,6 +240,7 @@ with `#`.
[CommonJS]: modules.md
[ES Modules]: esm.md
[Full TypeScript support]: #full-typescript-support
+[`--experimental-strip-private-modules`]: cli.md#--experimental-strip-private-modules
[`--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
diff --git a/doc/node-config-schema.json b/doc/node-config-schema.json
index 22523a4868f..6f809a0a355 100644
--- a/doc/node-config-schema.json
+++ b/doc/node-config-schema.json
@@ -367,6 +367,10 @@
"type": "boolean",
"description": "experimental iterable streams API (node:stream/iter)"
},
+ "experimental-strip-private-modules": {
+ "type": "boolean",
+ "description": "Type-stripping for TypeScript files under node_modules belonging to packages marked \"private\": true"
+ },
"experimental-vm-modules": {
"type": "boolean",
"description": "experimental ES Module support in vm module"
diff --git a/doc/node.1 b/doc/node.1
index 6294c2af42c..821fe8d8e0f 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -909,6 +909,18 @@ Enable experimental support for storage inspection
.It Fl -experimental-stream-iter
Enable the experimental \fBnode:stream/iter\fR module.
.
+.It Fl -experimental-strip-private-modules
+By default, Node.js refuses to strip types from TypeScript files inside
+folders under a \fBnode_modules\fR path. This flag enables type stripping for
+such files when they belong to a package whose \fBpackage.json\fR contains
+\fB"private": true\fR. Files belonging to non-private packages still throw
+\fBERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING\fR.
+The limitation to private packages should never be lifted,
+as publishing packages with TypeScript sources
+is highly discouraged.
+Stripping types inside \fBnode_modules\fR is intended only
+for local development.
+.
.It Fl -experimental-test-coverage
When used in conjunction with the \fBnode:test\fR module, a code coverage report is
generated as part of the test runner output. If no tests are run, a coverage
@@ -2273,6 +2285,8 @@ one is included in the list below.
.It
\fB--experimental-stream-iter\fR
.It
+\fB--experimental-strip-private-modules\fR
+.It
\fB--experimental-test-isolation\fR
.It
\fB--experimental-top-level-await\fR
diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js
index 98d9e04d0c9..851547d2216 100644
--- a/lib/internal/modules/helpers.js
+++ b/lib/internal/modules/helpers.js
@@ -85,6 +85,8 @@ const nativeLoaderMethods = {
modulesBinding.readPackageJSON(jsonPath, isESM, base, specifier),
getNearestParentPackageJSON: (checkPath) =>
modulesBinding.getNearestParentPackageJSON(checkPath),
+ getRootPackageJSON: (checkPath) =>
+ modulesBinding.getRootPackageJSON(checkPath),
getNearestParentPackageJSONType: (checkPath) =>
modulesBinding.getNearestParentPackageJSONType(checkPath),
getPackageScopeConfig: (resolved) =>
diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js
index ea6b7d26395..2ef5d1ebed3 100644
--- a/lib/internal/modules/package_json_reader.js
+++ b/lib/internal/modules/package_json_reader.js
@@ -64,6 +64,7 @@ function deserializePackageJSON(path, contents) {
3: plainImports,
4: plainExports,
5: optionalFilePath,
+ 6: isPrivate,
} = contents;
const pjsonPath = optionalFilePath ?? path;
@@ -73,6 +74,7 @@ function deserializePackageJSON(path, contents) {
...(name != null && { name }),
...(main != null && { main }),
...(type != null && { type }),
+ ...(isPrivate != null && { private: isPrivate }),
};
if (plainExports !== null) {
@@ -217,6 +219,40 @@ function getNearestParentPackageJSON(checkPath) {
return deserializedPackageJSONCache.get(parentPackageJSONPath);
}
+const moduleToRootPackageJSONCache = new SafeMap();
+
+/**
+ * This is stricter than {@link getNearestParentPackageJSON}: nested
+ * `package.json` files inside a package are ignored, and a non-private package
+ * anywhere in the `node_modules` chain (for example a publishable package that
+ * bundles a private dependency) disqualifies the path. A directory under
+ * `node_modules` with no `package.json` (such as pnpm's `.pnpm` store) is not
+ * a package and is skipped.
+ *
+ * Returns the package.json data and the path to the package.json file, or undefined.
+ * @param {string} checkPath The path to start searching from.
+ * @returns {undefined | DeserializedPackageConfig}
+ */
+function getRootPackageJSON(checkPath) {
+ const packageJSONPath = moduleToRootPackageJSONCache.get(checkPath);
+ if (packageJSONPath !== undefined) {
+ return deserializedPackageJSONCache.get(packageJSONPath);
+ }
+
+ const result = loaderMethods.getRootPackageJSON(checkPath);
+ const packageConfig = deserializePackageJSON(checkPath, result);
+
+ moduleToRootPackageJSONCache.set(checkPath, packageConfig.path);
+
+ const maybeCachedPackageConfig = deserializedPackageJSONCache.get(packageConfig.path);
+ if (maybeCachedPackageConfig !== undefined) {
+ return maybeCachedPackageConfig;
+ }
+
+ deserializedPackageJSONCache.set(packageConfig.path, packageConfig);
+ return packageConfig;
+}
+
/**
* Returns the package configuration for the given resolved URL.
* @param {URL | string} resolved - The resolved URL.
@@ -412,6 +448,7 @@ function purgePackageJSONCacheForPrefix(mountPoint) {
module.exports = {
read,
getNearestParentPackageJSON,
+ getRootPackageJSON,
getPackageScopeConfig,
getPackageType,
getPackageJSONURL,
diff --git a/lib/internal/modules/typescript.js b/lib/internal/modules/typescript.js
index 43bc57d977e..ebe41e308bc 100644
--- a/lib/internal/modules/typescript.js
+++ b/lib/internal/modules/typescript.js
@@ -18,6 +18,7 @@ const {
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX,
} = require('internal/errors').codes;
+const { getOptionValue } = require('internal/options');
const assert = require('internal/assert');
const {
getCompileCacheEntry,
@@ -141,6 +142,35 @@ function stripTypeScriptTypesForCoverage(code) {
}
+/**
+ * Whether type-stripping is allowed for files under node_modules belonging
+ * to packages whose package.json contains `"private": true`.
+ * @returns {boolean}
+ */
+const isStripPrivateModulesEnabled = getLazy(
+ () => getOptionValue('--experimental-strip-private-modules'),
+);
+
+/**
+ * Checks whether a file under node_modules is owned by a private package, in
+ * which case type stripping is allowed under `--experimental-strip-private-modules`.
+ *
+ * Every package enclosing the file inside `node_modules` must be marked
+ * `"private": true`. npm refuses to publish a private package, so requiring
+ * the package root (not a nested package.json, which is published verbatim)
+ * and rejecting any non-private enclosing package (e.g. a publishable package
+ * bundling a private dependency) prevents published packages from shipping
+ * strippable TypeScript. See {@link getRootPackageJSON}.
+ * @param {string} filename The filename (path or file URL) of the source code.
+ * @returns {boolean} Whether the file belongs to a package with `"private": true`.
+ */
+function isInsidePrivatePackage(filename) {
+ const { getRootPackageJSON } = require('internal/modules/package_json_reader');
+ const { urlToFilename } = require('internal/modules/helpers');
+ const packageJSON = getRootPackageJSON(urlToFilename(filename));
+ return packageJSON?.data.private === true;
+}
+
/**
* Performs type-stripping to TypeScript source code internally.
* It is used by internal loaders.
@@ -152,7 +182,10 @@ function stripTypeScriptTypesForCoverage(code) {
function stripTypeScriptModuleTypes(source, filename, sourceURL) {
assert(typeof source === 'string');
if (isUnderNodeModules(filename)) {
- throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
+ if (!isStripPrivateModulesEnabled() || !isInsidePrivatePackage(filename)) {
+ throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
+ }
+ emitExperimentalWarning('Type stripping in node_modules');
}
// Get a compile cache entry into the native compile cache store,
// keyed by the filename. If the cache can already be loaded on disk,
diff --git a/src/node_modules.cc b/src/node_modules.cc
index ec4f6bdd79a..f88fc461f4d 100644
--- a/src/node_modules.cc
+++ b/src/node_modules.cc
@@ -21,6 +21,7 @@ namespace node {
namespace modules {
using v8::Array;
+using v8::Boolean;
using v8::Context;
using v8::External;
using v8::FunctionCallbackInfo;
@@ -80,15 +81,18 @@ Local<Array> BindingData::PackageConfig::Serialize(Realm* realm) const {
isolate, input.data(), NewStringType::kNormal, input.size())
.ToLocalChecked();
};
- Local<Value> values[6] = {
+ Local<Value> 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),
+ is_private.has_value()
+ ? Boolean::New(isolate, *is_private).As<Primitive>()
+ : Undefined(isolate),
};
- return Array::New(isolate, values, 6);
+ return Array::New(isolate, values, 7);
}
bool BindingData::ParsePackageJSON(Realm* realm,
@@ -207,6 +211,11 @@ bool BindingData::ParsePackageJSON(Realm* realm,
if (field_value == "commonjs" || field_value == "module") {
package_config->type = field_value;
}
+ } else if (key == "private") {
+ bool is_private;
+ if (!value.get_bool().get(is_private)) {
+ package_config->is_private = is_private;
+ }
} else if (key == "scripts") {
if (value.type().get(field_type)) {
return throw_invalid_package_config();
@@ -405,6 +414,72 @@ const BindingData::PackageConfig* BindingData::TraverseParent(
return nullptr;
}
+const BindingData::PackageConfig* BindingData::FindNodeModulesPackage(
+ Realm* realm, const std::filesystem::path& check_path) {
+ std::filesystem::path current_path = check_path;
+ std::filesystem::path child;
+ std::filesystem::path grandchild;
+ const PackageConfig* innermost = nullptr;
+ auto env = realm->env();
+ const bool is_permissions_enabled = env->permission()->enabled();
+
+ while (true) {
+ auto parent_path = current_path.parent_path();
+ if (parent_path == current_path) {
+ // Reached the filesystem root.
+ break;
+ }
+
+ if (current_path.filename() == "node_modules") {
+ // `child` is the directory directly under node_modules. For scoped
+ // packages the package root is one level further down.
+ const std::string child_name = child.filename().generic_string();
+ const std::filesystem::path& package_root =
+ (!child_name.empty() && child_name[0] == '@') ? grandchild : child;
+ if (package_root.empty()) {
+ // The path points at a node_modules or bare scope directory rather
+ // than into a package.
+ return nullptr;
+ }
+
+ auto package_json_path = package_root / "package.json";
+
+ // GetPackageJSON()'s ReadFileSync() reads the file directly without
+ // consulting the permission model, so check read access here as
+ // TraverseParent() does. Without permission to confirm the package is
+ // private, fail closed and deny stripping.
+ if (is_permissions_enabled) [[unlikely]] {
+ if (!env->permission()->is_granted(
+ env,
+ permission::PermissionScope::kFileSystemRead,
+ ConvertGenericPathToUTF8(package_json_path))) {
+ return nullptr;
+ }
+ }
+
+ auto package_json =
+ GetPackageJSON(realm, ConvertPathToUTF8(package_json_path), nullptr);
+ if (package_json != nullptr) {
+ if (!package_json->is_private.value_or(false)) {
+ // A publishable (non-private) package encloses the file.
+ return nullptr;
+ }
+ if (innermost == nullptr) {
+ innermost = package_json;
+ }
+ }
+ // A directory under node_modules with no package.json is not a package
+ // (e.g. pnpm's `.pnpm` store); skip it and keep walking upwards.
+ }
+
+ grandchild = child;
+ child = current_path;
+ current_path = parent_path;
+ }
+
+ return innermost;
+}
+
const std::filesystem::path BindingData::NormalizePath(
Realm* realm, BufferValue* path_value) {
// Check if the path has a trailing slash. If so, add it after
@@ -439,6 +514,22 @@ void BindingData::GetNearestParentPackageJSON(
}
}
+void BindingData::GetRootPackageJSON(const FunctionCallbackInfo<Value>& args) {
+ CHECK_GE(args.Length(), 1);
+ CHECK(args[0]->IsString());
+
+ Realm* realm = Realm::GetCurrent(args);
+ BufferValue path_value(realm->isolate(), args[0]);
+
+ auto path = NormalizePath(realm, &path_value);
+
+ auto package_json = FindNodeModulesPackage(realm, path);
+
+ if (package_json != nullptr) {
+ args.GetReturnValue().Set(package_json->Serialize(realm));
+ }
+}
+
void BindingData::GetNearestParentPackageJSONType(
const FunctionCallbackInfo<Value>& args) {
CHECK_GE(args.Length(), 1);
@@ -696,6 +787,7 @@ void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
target,
"getNearestParentPackageJSON",
GetNearestParentPackageJSON);
+ SetMethod(isolate, target, "getRootPackageJSON", GetRootPackageJSON);
SetMethod(
isolate, target, "getPackageScopeConfig", GetPackageScopeConfig<false>);
SetMethod(isolate, target, "getPackageType", GetPackageScopeConfig<true>);
@@ -769,6 +861,7 @@ void BindingData::RegisterExternalReferences(
registry->Register(ParsePackageJSONBinding);
registry->Register(GetNearestParentPackageJSONType);
registry->Register(GetNearestParentPackageJSON);
+ registry->Register(GetRootPackageJSON);
registry->Register(GetPackageScopeConfig<false>);
registry->Register(GetPackageScopeConfig<true>);
registry->Register(EnableCompileCache);
diff --git a/src/node_modules.h b/src/node_modules.h
index 54417731265..997f428b3a1 100644
--- a/src/node_modules.h
+++ b/src/node_modules.h
@@ -33,6 +33,7 @@ class BindingData : public SnapshotableObject {
std::optional<std::string> exports;
std::optional<std::string> imports;
std::optional<std::string> scripts;
+ std::optional<bool> is_private;
std::string raw_json;
v8::Local<v8::Array> Serialize(Realm* realm) const;
@@ -62,6 +63,8 @@ class BindingData : public SnapshotableObject {
const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetNearestParentPackageJSON(
const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void GetRootPackageJSON(
+ const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetNearestParentPackageJSONType(
const v8::FunctionCallbackInfo<v8::Value>& args);
template <bool return_only_type>
@@ -103,6 +106,11 @@ class BindingData : public SnapshotableObject {
ErrorContext* error_context);
static const PackageConfig* TraverseParent(
Realm* realm, const std::filesystem::path& check_path);
+ // Returns the package.json of the package that owns check_path inside the
+ // innermost node_modules directory, or null when check_path is not inside
+ // node_modules or the package root has no package.json.
+ static const PackageConfig* FindNodeModulesPackage(
+ Realm* realm, const std::filesystem::path& check_path);
};
} // namespace modules
diff --git a/src/node_options.cc b/src/node_options.cc
index 6bf5a0270c7..d066f4af772 100644
--- a/src/node_options.cc
+++ b/src/node_options.cc
@@ -1479,6 +1479,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
kAllowedInEnvvar,
HAVE_AMARO);
AddAlias("--experimental-strip-types", "--strip-types");
+ AddOption("--experimental-strip-private-modules",
+ "Type-stripping for TypeScript files under node_modules "
+ "belonging to packages marked \"private\": true",
+ BOOL_FIELD(experimental_strip_private_modules),
+ kAllowedInEnvvar);
AddOption("--interactive",
"always enter the REPL even if stdin does not appear "
"to be a terminal",
diff --git a/src/node_options.h b/src/node_options.h
index bb11922f84f..b5f2084ab74 100644
--- a/src/node_options.h
+++ b/src/node_options.h
@@ -298,6 +298,8 @@ class EnvironmentOptions : public Options {
DEFINE_BOOL_FIELD(tls_max_v1_2) = false;
DEFINE_BOOL_FIELD(tls_max_v1_3) = false;
DEFINE_BOOL_FIELD(strip_types) = HAVE_AMARO;
+ DEFINE_BOOL_FIELD(experimental_strip_private_modules) =
+ EXPERIMENTALS_DEFAULT_VALUE;
DEFINE_BOOL_FIELD(report_exclude_env) = false;
DEFINE_BOOL_FIELD(report_exclude_network) = false;
#if HAVE_INSPECTOR
diff --git a/test/es-module/test-typescript.mjs b/test/es-module/test-typescript.mjs
index fd58d1d990a..07b556c2db1 100644
--- a/test/es-module/test-typescript.mjs
+++ b/test/es-module/test-typescript.mjs
@@ -61,6 +61,136 @@ test('execute a TypeScript file with node_modules', async () => {
assert.strictEqual(result.code, 0);
});
+test('execute a TypeScript file from a private package in node_modules', async () => {
+ const result = await spawnPromisified(process.execPath, [
+ '--experimental-strip-private-modules',
+ '--no-warnings',
+ fixtures.path('typescript/ts/test-typescript-private-node-modules.ts'),
+ ]);
+
+ assert.strictEqual(result.stderr, '');
+ assert.match(result.stdout, /Hello, TypeScript!/);
+ assert.strictEqual(result.code, 0);
+});
+
+test('execute a TypeScript file from a private package with a non-private nested package.json',
+ async () => {
+ const result = await spawnPromisified(process.execPath, [
+ '--experimental-strip-private-modules',
+ '--no-warnings',
+ fixtures.path('typescript/ts/test-typescript-nested-nonprivate-node-modules.ts'),
+ ]);
+
+ assert.strictEqual(result.stderr, '');
+ assert.match(result.stdout, /Hello, TypeScript!/);
+ assert.strictEqual(result.code, 0);
+ });
+
+test('emit an experimental warning when stripping types in a private node_modules package',
+ async () => {
+ const result = await spawnPromisified(process.execPath, [
+ '--experimental-strip-private-modules',
+ fixtures.path('typescript/ts/test-typescript-private-node-modules.ts'),
+ ]);
+
+ assert.match(result.stderr, /Type stripping in node_modules is an experimental feature/);
+ assert.match(result.stdout, /Hello, TypeScript!/);
+ assert.strictEqual(result.code, 0);
+ });
+
+test('require a TypeScript file from a private package in node_modules', async () => {
+ const result = await spawnPromisified(process.execPath, [
+ '--experimental-strip-private-modules',
+ '--no-warnings',
+ fixtures.path('typescript/ts/test-typescript-private-node-modules-require.ts'),
+ ]);
+
+ assert.strictEqual(result.stderr, '');
+ assert.match(result.stdout, /Hello, TypeScript!/);
+ assert.strictEqual(result.code, 0);
+});
+
+test('expect failure for a private package in node_modules without --experimental-strip-private-modules',
+ async () => {
+ const result = await spawnPromisified(process.execPath, [
+ '--no-warnings',
+ fixtures.path('typescript/ts/test-typescript-private-node-modules.ts'),
+ ]);
+
+ assert.match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/);
+ assert.strictEqual(result.stdout, '');
+ assert.strictEqual(result.code, 1);
+ });
+
+// A nested package.json must not be able to enable type stripping: npm only
+// refuses to publish packages whose root package.json has "private": true, so
+// a published package can freely ship a nested package.json with
+// "private": true. Only the package.json at the package root is consulted.
+test('expect failure for a nested package.json faking "private": true in node_modules',
+ async () => {
+ const result = await spawnPromisified(process.execPath, [
+ '--experimental-strip-private-modules',
+ '--no-warnings',
+ fixtures.path('typescript/ts/test-typescript-fake-private-node-modules.ts'),
+ ]);
+
+ assert.match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/);
+ assert.strictEqual(result.stdout, '');
+ assert.strictEqual(result.code, 1);
+ });
+
+test('expect failure for a private package bundled inside a non-private package',
+ async () => {
+ const result = await spawnPromisified(process.execPath, [
+ '--experimental-strip-private-modules',
+ '--no-warnings',
+ fixtures.path('typescript/ts/test-typescript-bundled-private-node-modules.ts'),
+ ]);
+
+ assert.match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/);
+ assert.strictEqual(result.stdout, '');
+ assert.strictEqual(result.code, 1);
+ });
+
+test('expect failure for a relative import into a non-private node_modules package',
+ async () => {
+ const result = await spawnPromisified(process.execPath, [
+ '--experimental-strip-private-modules',
+ '--no-warnings',
+ fixtures.path('typescript/ts/test-typescript-relative-node-modules.ts'),
+ ]);
+
+ assert.match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/);
+ assert.strictEqual(result.stdout, '');
+ assert.strictEqual(result.code, 1);
+ });
+
+test('expect failure for a ".." path resolving to a non-private node_modules package',
+ async () => {
+ const result = await spawnPromisified(process.execPath, [
+ '--experimental-strip-private-modules',
+ '--no-warnings',
+ fixtures.path('typescript/ts/test-typescript-dotdot-node-modules.ts'),
+ ]);
+
+ assert.match(result.stderr, /ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/);
+ assert.strictEqual(result.stdout, '');
+ assert.strictEqual(result.code, 1);
+ });
+
+test('expect failure for a non-private package in node_modules with --experimental-strip-private-modules',
+ async () => {
+ const result = await spawnPromisified(process.execPath, [
+ '--experimental-strip-private-modules',
+ '--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);
+ });
+
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'),
diff --git a/test/fixtures/typescript/ts/node_modules/bundler-pkg/index.js b/test/fixtures/typescript/ts/node_modules/bundler-pkg/index.js
new file mode 100644
index 00000000000..d342ad0fa36
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/bundler-pkg/index.js
@@ -0,0 +1 @@
+export { baz } from 'bundled-private';
diff --git a/test/fixtures/typescript/ts/node_modules/bundler-pkg/node_modules/bundled-private/bundled-private.ts b/test/fixtures/typescript/ts/node_modules/bundler-pkg/node_modules/bundled-private/bundled-private.ts
new file mode 100644
index 00000000000..746109acf8c
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/bundler-pkg/node_modules/bundled-private/bundled-private.ts
@@ -0,0 +1 @@
+export const baz: string = 'Hello, TypeScript!';
diff --git a/test/fixtures/typescript/ts/node_modules/bundler-pkg/node_modules/bundled-private/package.json b/test/fixtures/typescript/ts/node_modules/bundler-pkg/node_modules/bundled-private/package.json
new file mode 100644
index 00000000000..b0f08d814dc
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/bundler-pkg/node_modules/bundled-private/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "bundled-private",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "main": "bundled-private.ts"
+}
diff --git a/test/fixtures/typescript/ts/node_modules/bundler-pkg/package.json b/test/fixtures/typescript/ts/node_modules/bundler-pkg/package.json
new file mode 100644
index 00000000000..cd729ba337a
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/bundler-pkg/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "bundler-pkg",
+ "version": "1.0.0",
+ "type": "module",
+ "exports": "./index.js"
+}
diff --git a/test/fixtures/typescript/ts/node_modules/fake-private-pkg/lib/fake-private-pkg.ts b/test/fixtures/typescript/ts/node_modules/fake-private-pkg/lib/fake-private-pkg.ts
new file mode 100644
index 00000000000..746109acf8c
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/fake-private-pkg/lib/fake-private-pkg.ts
@@ -0,0 +1 @@
+export const baz: string = 'Hello, TypeScript!';
diff --git a/test/fixtures/typescript/ts/node_modules/fake-private-pkg/lib/package.json b/test/fixtures/typescript/ts/node_modules/fake-private-pkg/lib/package.json
new file mode 100644
index 00000000000..e986b24bbae
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/fake-private-pkg/lib/package.json
@@ -0,0 +1,4 @@
+{
+ "private": true,
+ "type": "module"
+}
diff --git a/test/fixtures/typescript/ts/node_modules/fake-private-pkg/package.json b/test/fixtures/typescript/ts/node_modules/fake-private-pkg/package.json
new file mode 100644
index 00000000000..1ef96e66059
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/fake-private-pkg/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "fake-private-pkg",
+ "version": "1.0.0",
+ "main": "lib/fake-private-pkg.ts"
+}
diff --git a/test/fixtures/typescript/ts/node_modules/nested-nonprivate-pkg/lib/index.ts b/test/fixtures/typescript/ts/node_modules/nested-nonprivate-pkg/lib/index.ts
new file mode 100644
index 00000000000..746109acf8c
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/nested-nonprivate-pkg/lib/index.ts
@@ -0,0 +1 @@
+export const baz: string = 'Hello, TypeScript!';
diff --git a/test/fixtures/typescript/ts/node_modules/nested-nonprivate-pkg/lib/package.json b/test/fixtures/typescript/ts/node_modules/nested-nonprivate-pkg/lib/package.json
new file mode 100644
index 00000000000..3dbc1ca591c
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/nested-nonprivate-pkg/lib/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "module"
+}
diff --git a/test/fixtures/typescript/ts/node_modules/nested-nonprivate-pkg/package.json b/test/fixtures/typescript/ts/node_modules/nested-nonprivate-pkg/package.json
new file mode 100644
index 00000000000..a0ba58582ef
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/nested-nonprivate-pkg/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "nested-nonprivate-pkg",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "exports": "./lib/index.ts"
+}
diff --git a/test/fixtures/typescript/ts/node_modules/private-pkg-cjs/package.json b/test/fixtures/typescript/ts/node_modules/private-pkg-cjs/package.json
new file mode 100644
index 00000000000..d6cdf88f497
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/private-pkg-cjs/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "private-pkg-cjs",
+ "version": "1.0.0",
+ "private": true,
+ "main": "private-pkg-cjs.ts"
+}
diff --git a/test/fixtures/typescript/ts/node_modules/private-pkg-cjs/private-pkg-cjs.ts b/test/fixtures/typescript/ts/node_modules/private-pkg-cjs/private-pkg-cjs.ts
new file mode 100644
index 00000000000..2d42fcdf192
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/private-pkg-cjs/private-pkg-cjs.ts
@@ -0,0 +1,3 @@
+const baz: string = 'Hello, TypeScript!';
+
+module.exports = { baz };
diff --git a/test/fixtures/typescript/ts/node_modules/private-pkg/greeting.ts b/test/fixtures/typescript/ts/node_modules/private-pkg/greeting.ts
new file mode 100644
index 00000000000..00b0820776c
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/private-pkg/greeting.ts
@@ -0,0 +1 @@
+export const greeting: string = 'Hello, TypeScript!';
diff --git a/test/fixtures/typescript/ts/node_modules/private-pkg/package.json b/test/fixtures/typescript/ts/node_modules/private-pkg/package.json
new file mode 100644
index 00000000000..2fd30de076e
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/private-pkg/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "private-pkg",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "main": "private-pkg.ts"
+}
diff --git a/test/fixtures/typescript/ts/node_modules/private-pkg/private-pkg.ts b/test/fixtures/typescript/ts/node_modules/private-pkg/private-pkg.ts
new file mode 100644
index 00000000000..25228dcb9ea
--- /dev/null
+++ b/test/fixtures/typescript/ts/node_modules/private-pkg/private-pkg.ts
@@ -0,0 +1,3 @@
+import { greeting } from './greeting.ts';
+
+export const baz: string = greeting;
diff --git a/test/fixtures/typescript/ts/test-typescript-bundled-private-node-modules.ts b/test/fixtures/typescript/ts/test-typescript-bundled-private-node-modules.ts
new file mode 100644
index 00000000000..6d57c6d8e04
--- /dev/null
+++ b/test/fixtures/typescript/ts/test-typescript-bundled-private-node-modules.ts
@@ -0,0 +1,3 @@
+import { baz } from 'bundler-pkg';
+
+console.log(baz);
diff --git a/test/fixtures/typescript/ts/test-typescript-dotdot-node-modules.ts b/test/fixtures/typescript/ts/test-typescript-dotdot-node-modules.ts
new file mode 100644
index 00000000000..5f9ed34c790
--- /dev/null
+++ b/test/fixtures/typescript/ts/test-typescript-dotdot-node-modules.ts
@@ -0,0 +1 @@
+require('./node_modules/private-pkg-cjs/../bar/bar.ts');
diff --git a/test/fixtures/typescript/ts/test-typescript-fake-private-node-modules.ts b/test/fixtures/typescript/ts/test-typescript-fake-private-node-modules.ts
new file mode 100644
index 00000000000..f340f6b5963
--- /dev/null
+++ b/test/fixtures/typescript/ts/test-typescript-fake-private-node-modules.ts
@@ -0,0 +1,3 @@
+import { baz } from 'fake-private-pkg';
+
+console.log(baz);
diff --git a/test/fixtures/typescript/ts/test-typescript-nested-nonprivate-node-modules.ts b/test/fixtures/typescript/ts/test-typescript-nested-nonprivate-node-modules.ts
new file mode 100644
index 00000000000..30c2c46864e
--- /dev/null
+++ b/test/fixtures/typescript/ts/test-typescript-nested-nonprivate-node-modules.ts
@@ -0,0 +1,3 @@
+import { baz } from 'nested-nonprivate-pkg';
+
+console.log(baz);
diff --git a/test/fixtures/typescript/ts/test-typescript-private-node-modules-require.ts b/test/fixtures/typescript/ts/test-typescript-private-node-modules-require.ts
new file mode 100644
index 00000000000..f2aa981bfaa
--- /dev/null
+++ b/test/fixtures/typescript/ts/test-typescript-private-node-modules-require.ts
@@ -0,0 +1,3 @@
+const { baz } = require('private-pkg-cjs');
+
+console.log(baz);
diff --git a/test/fixtures/typescript/ts/test-typescript-private-node-modules.ts b/test/fixtures/typescript/ts/test-typescript-private-node-modules.ts
new file mode 100644
index 00000000000..92a5cec8ecd
--- /dev/null
+++ b/test/fixtures/typescript/ts/test-typescript-private-node-modules.ts
@@ -0,0 +1,3 @@
+import { baz } from 'private-pkg';
+
+console.log(baz);
diff --git a/test/fixtures/typescript/ts/test-typescript-relative-node-modules.ts b/test/fixtures/typescript/ts/test-typescript-relative-node-modules.ts
new file mode 100644
index 00000000000..7691013129f
--- /dev/null
+++ b/test/fixtures/typescript/ts/test-typescript-relative-node-modules.ts
@@ -0,0 +1 @@
+require('./node_modules/bar/bar.ts');
diff --git a/typings/internalBinding/modules.d.ts b/typings/internalBinding/modules.d.ts
index 0b1d0e29383..73bfd2a5aca 100644
--- a/typings/internalBinding/modules.d.ts
+++ b/typings/internalBinding/modules.d.ts
@@ -5,6 +5,7 @@ export type PackageConfig = {
type: PackageType
exports?: string | string[] | Record<string, unknown>
imports?: string | string[] | Record<string, unknown>
+ private?: boolean
}
export type DeserializedPackageConfig = {
data: PackageConfig,
@@ -18,12 +19,14 @@ export type SerializedPackageConfig = [
string | undefined, // exports
string | undefined, // imports
DeserializedPackageConfig['path'], // pjson file path
+ PackageConfig['private'], // private
]
export interface ModulesBinding {
readPackageJSON(path: string): SerializedPackageConfig | undefined;
getNearestParentPackageJSONType(path: string): PackageConfig['type']
getNearestParentPackageJSON(path: string): SerializedPackageConfig | undefined
+ getRootPackageJSON(path: string): SerializedPackageConfig | undefined
getPackageScopeConfig(path: string): SerializedPackageConfig | undefined
getPackageType(path: string): PackageConfig['type'] | undefined
enableCompileCache(path?: string): { status: number, message?: string, directory?: string }