mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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>
This commit is contained in:
1 parent
135b70e670
commit
2052d0dcdf
36 files changed
+470
-3
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
+14
@@ -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
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+95
-2
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { baz } from 'bundled-private';
|
||||
test/fixtures/typescript/ts/node_modules/bundler-pkg/node_modules/bundled-private/bundled-private.ts
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export const baz: string = 'Hello, TypeScript!';
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "bundled-private",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "bundled-private.ts"
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "bundler-pkg",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"exports": "./index.js"
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export const baz: string = 'Hello, TypeScript!';
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module"
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "fake-private-pkg",
|
||||
"version": "1.0.0",
|
||||
"main": "lib/fake-private-pkg.ts"
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const baz: string = 'Hello, TypeScript!';
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "nested-nonprivate-pkg",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": "./lib/index.ts"
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "private-pkg-cjs",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "private-pkg-cjs.ts"
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
const baz: string = 'Hello, TypeScript!';
|
||||
|
||||
module.exports = { baz };
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const greeting: string = 'Hello, TypeScript!';
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "private-pkg",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "private-pkg.ts"
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { greeting } from './greeting.ts';
|
||||
|
||||
export const baz: string = greeting;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { baz } from 'bundler-pkg';
|
||||
|
||||
console.log(baz);
|
||||
@@ -0,0 +1 @@
|
||||
require('./node_modules/private-pkg-cjs/../bar/bar.ts');
|
||||
@@ -0,0 +1,3 @@
|
||||
import { baz } from 'fake-private-pkg';
|
||||
|
||||
console.log(baz);
|
||||
@@ -0,0 +1,3 @@
|
||||
import { baz } from 'nested-nonprivate-pkg';
|
||||
|
||||
console.log(baz);
|
||||
@@ -0,0 +1,3 @@
|
||||
const { baz } = require('private-pkg-cjs');
|
||||
|
||||
console.log(baz);
|
||||
@@ -0,0 +1,3 @@
|
||||
import { baz } from 'private-pkg';
|
||||
|
||||
console.log(baz);
|
||||
@@ -0,0 +1 @@
|
||||
require('./node_modules/bar/bar.ts');
|
||||
Vendored
+3
@@ -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 }
|
||||
|
||||
Reference in new issue
Block a user