Skip to content

Commit a50f611

Browse files
module: add --experimental-strip-private-modules
Signed-off-by: Marco Ippolito <marcoippolito54@gmail.com>
1 parent 2f2b810 commit a50f611

22 files changed

Lines changed: 228 additions & 3 deletions

doc/api/cli.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,6 +1407,25 @@ added:
14071407
14081408
Enable the experimental [`node:stream/iter`][] module.
14091409

1410+
### `--experimental-strip-private-modules`
1411+
1412+
<!-- YAML
1413+
added: REPLACEME
1414+
-->
1415+
1416+
> Stability: 1 - Experimental
1417+
1418+
By default, Node.js refuses to strip types from TypeScript files inside
1419+
folders under a `node_modules` path. This flag enables type stripping for
1420+
such files when they belong to a package whose `package.json` contains
1421+
`"private": true`. Files belonging to non-private packages still throw
1422+
[`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`][].
1423+
The limitation to private packages should never be lifted,
1424+
as publishing packages with TypeScript sources
1425+
is highly discouraged.
1426+
Stripping types inside `node_modules` is intended only
1427+
for local development.
1428+
14101429
### `--experimental-test-coverage`
14111430

14121431
<!-- YAML
@@ -3816,6 +3835,7 @@ one is included in the list below.
38163835
* `--experimental-shadow-realm`
38173836
* `--experimental-specifier-resolution`
38183837
* `--experimental-stream-iter`
3838+
* `--experimental-strip-private-modules`
38193839
* `--experimental-test-isolation`
38203840
* `--experimental-top-level-await`
38213841
* `--experimental-vfs`
@@ -4447,6 +4467,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
44474467
[`Buffer`]: buffer.md#class-buffer
44484468
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man3.0/man3/CRYPTO_secure_malloc_init.html
44494469
[`ERR_INVALID_TYPESCRIPT_SYNTAX`]: errors.md#err_invalid_typescript_syntax
4470+
[`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`]: errors.md#err_unsupported_node_modules_type_stripping
44504471
[`ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`]: errors.md#err_unsupported_typescript_syntax
44514472
[`NODE_OPTIONS`]: #node_optionsoptions
44524473
[`NODE_USE_ENV_PROXY=1`]: #node_use_env_proxy1

doc/api/packages.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,9 @@ The following fields in `package.json` files are used in Node.js:
11511151
limits which submodules can be loaded from within the package.
11521152
* [`"imports"`][] - Package imports, for use by modules within the package
11531153
itself.
1154+
* [`"private"`][] - When `true`, marks the package as not for publication. Used
1155+
by [`--experimental-strip-private-modules`][] to allow type stripping for the
1156+
package's TypeScript files under `node_modules`.
11541157

11551158
### `"name"`
11561159

@@ -1346,6 +1349,30 @@ Package imports permit mapping to external packages.
13461349

13471350
This field defines [subpath imports][] for the current package.
13481351

1352+
### `"private"`
1353+
1354+
<!-- YAML
1355+
added: REPLACEME
1356+
-->
1357+
1358+
* Type: {boolean}
1359+
1360+
```json
1361+
{
1362+
"private": true
1363+
}
1364+
```
1365+
1366+
The `"private"` field marks a package as private, signaling to package managers
1367+
such as _npm_ that it must not be published to a public registry.
1368+
By default, Node.js refuses to strip types from TypeScript files inside
1369+
`node_modules`. When [`--experimental-strip-private-modules`][] is enabled,
1370+
Node.js allows type stripping for:
1371+
1372+
* files that are not descendant of a `node_modules` directory.
1373+
* files that are descendant of a directory whose parent is a `node_modules` directory
1374+
and that contains a `package.json` file that sets `"private": true`.
1375+
13491376
[CommonJS]: modules.md
13501377
[Conditional exports]: #conditional-exports
13511378
[ES module]: esm.md
@@ -1360,10 +1387,12 @@ This field defines [subpath imports][] for the current package.
13601387
[`"imports"`]: #imports
13611388
[`"main"`]: #main
13621389
[`"name"`]: #name
1390+
[`"private"`]: #private
13631391
[`"type"`]: #type
13641392
[`--conditions` / `-C` flag]: #resolving-user-conditions
13651393
[`--experimental-addon-modules`]: cli.md#--experimental-addon-modules
13661394
[`--experimental-package-map`]: cli.md#--experimental-package-mappath
1395+
[`--experimental-strip-private-modules`]: cli.md#--experimental-strip-private-modules
13671396
[`--no-addons` flag]: cli.md#--no-addons
13681397
[`ERR_PACKAGE_MAP_EXTERNAL_FILE`]: errors.md#err_package_map_external_file
13691398
[`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported

doc/api/typescript.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,15 @@ To discourage package authors from publishing packages written in TypeScript,
217217
Node.js refuses to handle TypeScript files inside folders under a `node_modules`
218218
path.
219219

220+
The [`--experimental-strip-private-modules`][] flag relaxes this restriction
221+
for packages whose `package.json` contains `"private": true`, such as
222+
workspace packages in a monorepo, which are not published to a registry.
223+
The limitation to private packages should never be lifted,
224+
as publishing packages with TypeScript sources
225+
is highly discouraged.
226+
Stripping types inside `node_modules` is intended only
227+
for local development.
228+
220229
### Paths aliases
221230

222231
[`tsconfig` "paths"][] won't be transformed and therefore produce an error. The closest
@@ -226,6 +235,7 @@ with `#`.
226235
[CommonJS]: modules.md
227236
[ES Modules]: esm.md
228237
[Full TypeScript support]: #full-typescript-support
238+
[`--experimental-strip-private-modules`]: cli.md#--experimental-strip-private-modules
229239
[`--no-strip-types`]: cli.md#--no-strip-types
230240
[`ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`]: errors.md#err_unsupported_typescript_syntax
231241
[`tsconfig` "paths"]: https://www.typescriptlang.org/tsconfig/#paths

doc/node.1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,10 @@ Use this flag to enable ShadowRealm support.
777777
.It Fl -experimental-storage-inspection
778778
Enable experimental support for storage inspection
779779
.
780+
.It Fl -experimental-strip-private-modules
781+
Enable type-stripping for TypeScript files under node_modules belonging to
782+
packages whose package.json contains \fB"private": true\fR.
783+
.
780784
.It Fl -experimental-test-coverage
781785
When used in conjunction with the \fBnode:test\fR module, a code coverage report is
782786
generated as part of the test runner output. If no tests are run, a coverage

lib/internal/modules/package_json_reader.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ function deserializePackageJSON(path, contents) {
6060
3: plainImports,
6161
4: plainExports,
6262
5: optionalFilePath,
63+
6: isPrivate,
6364
} = contents;
6465

6566
const pjsonPath = optionalFilePath ?? path;
@@ -69,6 +70,7 @@ function deserializePackageJSON(path, contents) {
6970
...(name != null && { name }),
7071
...(main != null && { main }),
7172
...(type != null && { type }),
73+
...(isPrivate != null && { private: isPrivate }),
7274
};
7375

7476
if (plainExports !== null) {

lib/internal/modules/typescript.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const {
1818
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
1919
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX,
2020
} = require('internal/errors').codes;
21+
const { getOptionValue } = require('internal/options');
2122
const assert = require('internal/assert');
2223
const {
2324
getCompileCacheEntry,
@@ -141,6 +142,28 @@ function stripTypeScriptTypesForCoverage(code) {
141142
}
142143

143144

145+
/**
146+
* Whether type-stripping is allowed for files under node_modules belonging
147+
* to packages whose package.json contains `"private": true`.
148+
* @returns {boolean}
149+
*/
150+
const isStripPrivateModulesEnabled = getLazy(
151+
() => getOptionValue('--experimental-strip-private-modules'),
152+
);
153+
154+
/**
155+
* Checks if the nearest parent package.json of a file marks its package
156+
* as private.
157+
* @param {string} filename The filename (path or file URL) of the source code.
158+
* @returns {boolean} Whether the file belongs to a package with `"private": true`.
159+
*/
160+
function isInsidePrivatePackage(filename) {
161+
const { getNearestParentPackageJSON } = require('internal/modules/package_json_reader');
162+
const { urlToFilename } = require('internal/modules/helpers');
163+
const packageJSON = getNearestParentPackageJSON(urlToFilename(filename));
164+
return packageJSON?.data.private === true;
165+
}
166+
144167
/**
145168
* Performs type-stripping to TypeScript source code internally.
146169
* It is used by internal loaders.
@@ -152,7 +175,10 @@ function stripTypeScriptTypesForCoverage(code) {
152175
function stripTypeScriptModuleTypes(source, filename, sourceURL) {
153176
assert(typeof source === 'string');
154177
if (isUnderNodeModules(filename)) {
155-
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
178+
if (!isStripPrivateModulesEnabled() || !isInsidePrivatePackage(filename)) {
179+
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
180+
}
181+
emitExperimentalWarning('Type stripping in node_modules');
156182
}
157183
// Get a compile cache entry into the native compile cache store,
158184
// keyed by the filename. If the cache can already be loaded on disk,

src/node_modules.cc

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ namespace node {
2121
namespace modules {
2222

2323
using v8::Array;
24+
using v8::Boolean;
2425
using v8::Context;
2526
using v8::External;
2627
using v8::FunctionCallbackInfo;
@@ -80,15 +81,18 @@ Local<Array> BindingData::PackageConfig::Serialize(Realm* realm) const {
8081
isolate, input.data(), NewStringType::kNormal, input.size())
8182
.ToLocalChecked();
8283
};
83-
Local<Value> values[6] = {
84+
Local<Value> values[7] = {
8485
name.has_value() ? ToString(*name) : Undefined(isolate),
8586
main.has_value() ? ToString(*main) : Undefined(isolate),
8687
ToString(type),
8788
imports.has_value() ? ToString(*imports) : Undefined(isolate),
8889
exports.has_value() ? ToString(*exports) : Undefined(isolate),
8990
ToString(file_path),
91+
is_private.has_value()
92+
? Boolean::New(isolate, *is_private).As<Primitive>()
93+
: Undefined(isolate),
9094
};
91-
return Array::New(isolate, values, 6);
95+
return Array::New(isolate, values, 7);
9296
}
9397

9498
const BindingData::PackageConfig* BindingData::GetPackageJSON(
@@ -228,6 +232,11 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON(
228232
if (field_value == "commonjs" || field_value == "module") {
229233
package_config.type = field_value;
230234
}
235+
} else if (key == "private") {
236+
bool is_private;
237+
if (!value.get_bool().get(is_private)) {
238+
package_config.is_private = is_private;
239+
}
231240
} else if (key == "scripts") {
232241
if (value.type().get(field_type)) {
233242
return throw_invalid_package_config();

src/node_modules.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class BindingData : public SnapshotableObject {
3333
std::optional<std::string> exports;
3434
std::optional<std::string> imports;
3535
std::optional<std::string> scripts;
36+
std::optional<bool> is_private;
3637
std::string raw_json;
3738

3839
v8::Local<v8::Array> Serialize(Realm* realm) const;

src/node_options.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
12041204
kAllowedInEnvvar,
12051205
HAVE_AMARO);
12061206
AddAlias("--experimental-strip-types", "--strip-types");
1207+
AddOption("--experimental-strip-private-modules",
1208+
"Type-stripping for TypeScript files under node_modules "
1209+
"belonging to packages marked \"private\": true",
1210+
&EnvironmentOptions::experimental_strip_private_modules,
1211+
kAllowedInEnvvar);
12071212
AddOption("--interactive",
12081213
"always enter the REPL even if stdin does not appear "
12091214
"to be a terminal",

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ class EnvironmentOptions : public Options {
273273
std::vector<std::string> preload_esm_modules;
274274

275275
bool strip_types = HAVE_AMARO;
276+
bool experimental_strip_private_modules = EXPERIMENTALS_DEFAULT_VALUE;
276277

277278
std::vector<std::string> user_argv;
278279

0 commit comments

Comments
 (0)