@effected/npm

中文文档
0.34.1发布于 2026/11/13MITTypeScriptES Module构建来源已验证近 30 天下载 1.1万

Effect service contracts for resolving pnpm catalog: and workspace: dependency specifiers.

原始使用文档
文档目录13 节
  1. Why @effected/npm
  2. Install
  3. Quick start
  4. The contracts
  5. Implementing a resolver
  6. Manifest-level resolution
  7. Registry reads and publishing
  8. How a registry is authenticated
  9. Reading a published tarball
  10. Integrity hashes
  11. Who consumes this
  12. Features
  13. License

@effected/npm

npm License: MIT Node.js %3E%3D24.11.0 TypeScript 7.0

Effect service contracts for resolving pnpm catalog: and workspace: dependency specifiers, plus the npm registry and publish surface built on top of them. CatalogResolver.rangeOf turns a package name plus an optional catalog name into the configured range; WorkspaceResolver.versionOf turns a workspace package name into its concrete version. Both are Context.Service contracts, both ship a no-op default layer that resolves nothing, and neither one reads a file. The package is the seam: a library that needs to expand a specifier depends on this, and something that can actually see the workspace supplies the implementation. On top of the contracts sit the kit's shared dependency vocabulary — DependencySpecifier, DependencySection, IntegrityHash — and Manifest, a tolerant manifest model that resolves every catalog: and workspace: specifier through the contracts in one call. NpmRegistry and PackagePublish round out the package with the registry reads and pack/publish workflow a release tool needs, over core HttpClient and @effected/commands.

Pre-1.0.0. This package is part of the @effected/* kit, built on stable Effect v4 (effect ~4.1.0) and still in 0.x development. Stable Effect makes a kit 1.0.0 possible, not automatic. To keep your effect and @effect/* versions on the line the kit is built and tested against, install @effected/pnpm-plugin-effected.

Stability: unstable. This package's API surface is not yet considered complete and may change across 0.x releases. Pin an exact version — even a package marked stable before 1.0.0 can introduce a breaking change by accident, and an exact pin turns that into a type-check error rather than a runtime surprise. Full policy: release strategy.

Why @effected/npm

catalog: and workspace: are specifiers whose meaning lives somewhere other than the manifest they appear in. A package.json document library can parse "effect": "catalog:" perfectly well and still have no way to say what it resolves to, because the answer is in a workspace file it has no business reading. The usual escape hatches are both bad: take a dependency on a workspace crawler and drag a filesystem into a pure document library, or accept a resolver as an untyped callback and lose the failure channel. Neither is a contract. This package is the contract — shape only, so @effected/package-json can express Package.resolve against services in R and let the application decide where resolution actually comes from.

The one design decision worth stating up front: an unmatched specifier is Option.none(), not an error. A package that is absent from a catalog, or a name that is not a workspace member, is an ordinary answer and the resolver succeeds with None. The error channel is reserved for the resolution mechanism failing — an unreadable catalog file, a malformed workspace manifest — and DependencyResolutionError carries that cause structurally on a Schema.Defect field rather than folding it into a string. Blurring those two is how "no such package" ends up indistinguishable from "your disk is on fire".

Install

npm install @effected/npm effect
pnpm add @effected/npm effect

Requires Node.js >=24.11.0.

All @effected/* packages are ESM-only: the exports maps publish only import conditions, so require() — including tools that resolve in CJS mode — fails with Node's ERR_PACKAGE_PATH_NOT_EXPORTED rather than loading a CJS build that does not exist. Import from an ES module.

effect v4 and @effected/semver are peer dependencies; @effected/commands arrives as an ordinary dependency, behind PackagePublish. Defining the resolver contracts runs no effect and touches no filesystem — their default layers are Layer.succeed over functions that return Option.none() — but NpmRegistry needs a HttpClient and PackagePublish and PackageTarball need FileSystem, Digest.Digest (effect/crypto) and a ChildProcessSpawner, each supplied at the edge from @effect/platform-node or @effect/platform-bun — Digest.Digest from a platform crypto family layer such as NodeCrypto.layerDigest, or core's Digest.layerWebCrypto, since the platform *Services layers carry no crypto.

Quick start

The Default layer merges both no-op resolvers. Provide it when the contracts need to be satisfied but nothing should be resolved:

import { CatalogResolver, Default, WorkspaceResolver } from "@effected/npm";
import { Effect, Option } from "effect";

const program = Effect.gen(function* () {
  const catalog = yield* CatalogResolver;
  const workspace = yield* WorkspaceResolver;
  return yield* Effect.all([
    catalog.rangeOf("effect", Option.none()),
    workspace.versionOf("@effected/semver"),
  ]);
});

Effect.runPromise(Effect.provide(program, Default)).then(console.log);
// => [Option.none(), Option.none()]

Option.none() for the catalog argument selects the default catalog; Option.some("build") selects the named one.

The contracts

ServiceMethodReturns
CatalogResolverrangeOf(packageName, catalog: Option<string>)Option<string> — the configured range, or None when the package is not in that catalog
WorkspaceResolverversionOf(packageName)Option<string> — the concrete version with the range modifier stripped, or None when the name is not a workspace member

Both fail with DependencyResolutionError, which carries the specifier that could not be resolved and the structured cause of the underlying failure; rangeOf can additionally fail with CatalogAssemblyError when the catalogs could not be assembled from their sources. Both ship a noop layer bound to a const, so it memoizes by reference rather than minting a fresh layer at every use.

Implementing a resolver

A real resolver is a Layer.succeed over the shape. @effected/workspaces implements these against a discovered monorepo, but the contract is small enough that a fixed record is a legitimate implementation — for a test, a fixture, or a tool that already knows its own catalog:

import { CatalogResolver, WorkspaceResolver } from "@effected/npm";
import { Effect, Layer, Option } from "effect";

const catalogs: Record<string, Record<string, string>> = {
  default: { effect: "^4.0.0" },
  build: { typescript: "^6.0.0" },
};

const workspaceVersions: Record<string, string> = { "@acme/widget": "1.4.0" };

export const ResolversLive = Layer.mergeAll(
  Layer.succeed(CatalogResolver, {
    rangeOf: (packageName, catalog) =>
      Effect.succeed(Option.fromUndefinedOr(catalogs[Option.getOrElse(catalog, () => "default")]?.[packageName])),
  }),
  Layer.succeed(WorkspaceResolver, {
    versionOf: (packageName) => Effect.succeed(Option.fromUndefinedOr(workspaceVersions[packageName])),
  }),
);

Note what the implementation does not do: an unknown package name returns Option.none() and never fails. Save the error channel for the case where you tried to read the catalog and could not.

Manifest-level resolution

Manifest is a tolerant manifest model over the contracts: the four dependency fields are typed as string→string records, and every other top-level field rides through a rest catch-all that flattens back on encode, so the wire shape never carries a literal rest key. It is deliberately not @effected/package-json's strict Package — a mid-build manifest is an arbitrary user record, and resolution has no business validating fields it never reads.

import { Default, Manifest } from "@effected/npm";
import { Effect } from "effect";

const program = Effect.gen(function* () {
  const manifest = yield* Manifest.decode({ name: "app", dependencies: { effect: "^4.0.0" } });
  const resolved = manifest.needsResolution ? yield* manifest.resolve() : manifest;
  return resolved.toRecord();
});

Effect.runPromise(Effect.provide(program, Default)).then(console.log);
// => { dependencies: { effect: "^4.0.0" }, name: "app" }

needsResolution is a pure getter: check it first and skip resolution entirely (and whatever catalog assembly backs the resolvers) when no dependency field carries a catalog: or workspace: specifier. resolve() projects every such specifier through the contracts and returns a new Manifest, applying pnpm's publish-time semantics — the alias form workspace:<name>@<range> resolves the target package's version and becomes the npm:<name>@<range> alias pnpm publishes. At the manifest level a specifier the resolvers answer Option.none() for fails typed as UnresolvedDependencyError, naming the field, the dependency and the reason: an unmatched entry is an ordinary answer for a resolver, but it means the manifest cannot be projected.

Registry reads and publishing

NpmRegistry replaces every shelled npm view, reading over core HttpClient instead. The registry is a per-call argument rather than baked into the layer, so one program can probe two registries for the same package, and an absent package or version is Option.none() rather than an error:

import { NpmRegistry } from "@effected/npm";
import { FetchHttpClient } from "effect/http";
import { Effect } from "effect";

const program = Effect.gen(function* () {
  const registry = yield* NpmRegistry;
  return yield* registry.version("effect", "4.0.0");
});

Effect.runPromise(program.pipe(Effect.provide(NpmRegistry.layer), Effect.provide(FetchHttpClient.layer)));
// Option.some(PublishedVersion) when that version is on the registry, Option.none() otherwise

A github-packages target reads through the packument instead: that registry answers the per-version endpoint with a 405 whatever the credentials, so version routes there up front — and any other registry that answers 405 falls back the same way, so "is this version published" gives the same answer everywhere.

manifest(name, version) reads the dependency fields of one version by that same route: a VersionManifest carrying dependencies, peerDependencies, optionalDependencies, peerDependenciesMeta and engines, each absent when the document does not declare it. A resolver that lists versions and reads their peers therefore goes through one registry, one credential and one routing rule rather than two that can disagree:

import { NpmRegistry } from "@effected/npm";
import { Effect, Option } from "effect";

const peersOf = (name: string, version: string) =>
  Effect.gen(function* () {
    const registry = yield* NpmRegistry;
    const found = yield* registry.manifest(name, version);
    return Option.map(found, (manifest) => manifest.peerDependencies ?? {});
  });

A malformed dependency record fails RegistryReadError with kind "decode" rather than reading as "no peers"; an engines value in the legacy array form old publishes carry is omitted instead, so it cannot fail the read of a version that resolves fine. VersionManifest is also the schema: decoding a version document with it is the same check, except that it rejects the legacy engines form.

NpmConfig.load({ cwd, env }) resolves the npm configuration a read should use — the project, user and global npmrc plus npm_config_* — and config.targetFor(name) hands NpmRegistry the package's registry and the credential configured for that host, never another's. The project npmrc is found the way npm finds it, in the nearest directory holding a package.json or node_modules. In a monorepo that is the wrong file when the program runs from a member package, so two options move it: workspaceRoot: true reads it at the nearest directory holding a pnpm-workspace.yaml or a package.json with a workspaces field (falling back to the default when there is none), and projectDir names the directory outright, winning over workspaceRoot:

import { NpmConfig, NpmRegistry } from "@effected/npm";
import { Effect } from "effect";

const versionsFromAnywhere = (name: string, cwd: string, env: Record<string, string | undefined>) =>
  Effect.gen(function* () {
    const config = yield* NpmConfig.load({ cwd, env, workspaceRoot: true });
    const registry = yield* NpmRegistry;
    return yield* registry.versions(name, config.targetFor(name));
  });
// needs FileSystem and Path for the load, and HttpClient for the read, at the edge

PackagePublish runs the rest of a release through @effected/commands' Run: pack writes the tarball and reports both its npm integrity and a local sha256 (the attestation subject), publishTarball uploads bytes packed earlier, and dryRun reports packability without contacting a registry. publishTarball's PublishOutcome reports auth — "oidc-exchange", "configured-credential" or "unknown" — read from npm's own log lines. npm names its auth path only below its default log level, so the publish runs at --loglevel=silly, which overrides a loglevel set in an npmrc or the environment (only a --loglevel in the executor's extraArgs comes later and wins, and that run reports "unknown"); a failure's PublishError.output is filtered back to npm's default levels. tokenAuth: true blanks NPM_ID_TOKEN and the Actions token-service variables so npm publishes with the npmrc credential, and drops --provenance, which on GitHub Actions needs that token service. Every step fails typed as PublishError, routed by kind, rather than throwing on a non-zero npm exit. NpmExecutor.ambient runs the runner's own npm; NpmExecutor.dlx("npm@11") fetches a pinned one through the project's launcher, which is what OIDC trusted publishing needs on runners that still ship npm 10.x.

NpmExecutor is a value you copy rather than a flag bag: withCacheDir(dir) redirects npm's cache — worth doing on a runner whose $HOME is not writable, or where the cache belongs on the same volume as the workspace — and withExtraArgs(args) is the generic vent for a flag this package has not named. Both return a new executor, and withExtraArgs replaces rather than accumulates, so a copy is a complete statement of its own flags. --cache outranks both npm_config_cache and an npmrc cache, so withCacheDir overrides a cache someone configured deliberately — the combinator stays dumb about ambient state, and checking for one is the caller's call.

import { NpmExecutor } from "@effected/npm";

declare const runnerTemp: string;

const executor = NpmExecutor.dlx("npm@11").withCacheDir(`${runnerTemp}/npm-cache`);
// every invocation now carries `--cache <runnerTemp>/npm-cache`, after its own args

classifyRegistry tells npm, GitHub Packages, JSR and a custom registry apart as a RegistryKind, since provenance and auth differ by kind, and three projections render one for a human: registryShortLabel (npm, github, jsr, or the host) for a log line or a check name, registryDisplayName (npm, GitHub Packages, JSR, or the host) for prose, and registryHost for the bare host of any URL.

How a registry is authenticated

RegistryCredential is the closed union both halves of the registry surface speak — { kind: "token", token } for npm's _authToken, and { kind: "basic", encoded } for its _auth, carried already base64-encoded because that is what npm stores and what registry configuration in the wild already contains. basicCredentialFromPairResult(username, password) encodes for a caller that genuinely holds a pair, returning a Result that fails with BasicCredentialError (reason "colon-in-username") rather than throwing; the secret is Redacted on both sides of it.

import { PackagePublish, basicCredentialFromPairResult } from "@effected/npm";
import { Effect, Redacted } from "effect";

declare const password: Redacted.Redacted<string>;

const program = Effect.gen(function* () {
  const credential = yield* Effect.fromResult(basicCredentialFromPairResult("ci", password));
  const publish = yield* PackagePublish;
  yield* publish.setupAuth({
    registry: "https://npm.internal.example",
    credential,
    npmrcPath: "/tmp/.npmrc",
  });
});
// appends `//npm.internal.example/:_auth=<encoded>` to the npmrc, creating it when absent
// Failure: BasicCredentialError when the username contains a colon

basicCredentialFromPairResult replaces the throwing basicCredentialFromPair; the rename is the migration signal, because a same-named function whose return type changed would still compile at a loosely typed call site.

setupAuth also takes a third, publish-only arm: WorkloadCredential, { kind: "workload", audience, token }, for a registry that accepts a CI workload's identity token behind a registry-specific prefix. The pure workloadCredential({ audience, prefix, identityToken }) builds one without the caller holding the plaintext, and setupAuth writes it under _authToken. setupAuth's parameter type is PublishCredential, the union of all three; RegistryTarget.credential stays RegistryCredential, so passing a workload credential to a read fails to compile, and a read handed one through a cast fails RegistryReadError with kind "credential" before any request goes out. Minting and masking the identity token are the caller's job; on GitHub Actions, ActionsRegistryCredential.workload from @effected/github-actions does both.

One union rather than two spellings is the point: NpmRegistry's per-call RegistryTarget takes the same credential, so a read probe and a publish cannot disagree about the scheme for one registry. A bearer probe against a basic-auth registry answers 401, version reads that as "not published", and a publish flow acting on it republishes a version that already exists.

Breaking, and how to migrate. setupAuth and RegistryTarget took a token field before; both take credential now. The rewrite is mechanical:

import type { RegistryCredential } from "@effected/npm";
import type { Redacted } from "effect";

declare const token: Redacted.Redacted<string>;

// before: setupAuth({ registry, token, npmrcPath })
const credential: RegistryCredential = { kind: "token", token };
// after:  setupAuth({ registry, credential, npmrcPath })

RegistryTarget.token survives for one minor typed as never, deliberately, so the break is loud. Callers commonly pass the field through a conditional spread (...(token !== null ? { token } : {})), and a spread of a no-longer-known property is not an excess-property error — the field would simply vanish and an authenticated probe would silently become an anonymous one. Typed never, the same spread fails to compile.

Reading a published tarball

PackageTarball is the inbound half of the registry surface: NpmRegistry reads metadata and PackagePublish sends a tarball out, but nothing read a published one back. extract downloads a version, verifies the bytes against the integrity the registry vouched for, unpacks it, and answers the directory the package/ root landed in — as a scoped resource, so the temporary directory is removed when the calling scope closes:

import { NpmRegistry, PackageTarball } from "@effected/npm";
import { Effect, Option } from "effect";

const read = Effect.gen(function* () {
  const registry = yield* NpmRegistry;
  const tarball = yield* PackageTarball;
  const found = yield* registry.version("some-config", "1.2.3");
  if (Option.isNone(found)) return Option.none();
  return Option.some(yield* tarball.extract(found.value));
}).pipe(Effect.scoped);
// Option.some("<a temporary directory>") — removed when the scope closes

Verification happens before extraction and before anything reads the contents, so a poisoned intermediary — a CDN edge, a proxy, a mirror — never reaches tar. TarballError carries a reason that keeps the outcomes apart: notFound (no recorded tarball, or a 404), http (any other transport or non-2xx failure), integrityMismatch (with expected and actual) and extractFailed. The notFound split is load-bearing — a consumer that cannot tell "this version legitimately does not exist" from "something went wrong fetching a version that does" has to treat both alike, and that failure is silent.

Extraction shells out to tar through core's ChildProcessSpawner rather than taking a tarball-reader dependency, which is a tier decision: a non-core runtime dependency here would make this package integrated, and that propagates to the pure packages that depend on it for vocabulary. tar is on every CI runner image; a consumer off a runner needs both a spawner and the binary. Pair it with resolveEntryPoint from @effected/package-json to find the entry file inside the extracted directory — loading that file is deliberately not part of this surface, since a dynamic import() of a computed path becomes a bundler context module with no seam to fix it.

Integrity hashes

IntegrityHash is a brand over the three textual integrity forms a dependency graph carries — npm's SRI sha512-<base64>, corepack's sha512.<hex> and yarn's — with algorithmOf reading the algorithm out of any of them. CorepackIntegrityHash narrows the brand to the corepack spelling, which is what a packageManager pin has to be written in, and carries the bridge from the form npm actually hands you:

import { CorepackIntegrityHash } from "@effected/npm";
import { Effect } from "effect";

declare const npmIntegrity: string; // "sha512-<base64>", as the registry reports it

Effect.runPromise(CorepackIntegrityHash.fromSri(npmIntegrity)).then(console.log);
// "sha512.<the same digest, lowercase hex>" — the spelling a packageManager pin takes
// Failure: InvalidSriIntegrityHashError, carrying the offending `input`

FromSri is the same conversion as a Schema.Codec, for decoding an integrity field inside your own schemas; fromSri is the Effect convenience over it. The bridge is deliberately one-way: an input that is already in the corepack form fails typed rather than passing through, so a caller feeding pins back into the converter finds the wiring bug instead of masking it. The base64 reader is strict for the same reason — a lenient one mints pins corepack rejects at install time. Padding is its one latitude: a digest's padded and unpadded spellings decode to the same pin, while a stray character, an interior =, or non-zero trailing bits all fail.

Who consumes this

@effected/package-json is the reason the package exists. Package.resolve walks all four dependency maps, expands every catalog: and workspace: specifier through these two services, and leaves untouched any specifier the resolvers answered None for. @effected/lockfiles came second, which is why the specifier and integrity vocabulary lives here rather than in package-json. @effected/workspaces sits on the other side of the seam and provides the implementations that read a real pnpm workspace — its Workspaces.resolveManifest runs Manifest.resolve() for you over a freshly discovered workspace. Nothing in this package points outward at any of them.

Features

  • CatalogResolver — the catalog: contract, plus CatalogResolver.noop.
  • WorkspaceResolver — the workspace: contract, plus WorkspaceResolver.noop.
  • DependencyResolutionError / CatalogAssemblyError — the typed failures: the resolution mechanism broke, or the catalogs could not be assembled from their sources.
  • Default — Layer.mergeAll of the two no-op layers, for when the types need satisfying and nothing needs resolving.
  • Manifest — the tolerant manifest model: Manifest.decode, the pure needsResolution fast path, resolve() over the contracts and toRecord() back to the wire shape, with ManifestDecodeError and UnresolvedDependencyError as its typed failures.
  • DependencySpecifier — the specifier taxonomy: an eleven-protocol classifier, a codec decoding any specifier into a matchable tagged union that encodes back byte-for-byte, and the resolution statics (catalogNameOf, resolveWorkspace, workspaceTargetOf) implementing pnpm's publish-time projection.
  • DependencySection — the kit-wide dependency vocabulary: DependencyKind, DependencyField and the mapping between them.
  • IntegrityHash — a brand over the three textual integrity forms (SRI, corepack, yarn), with algorithmOf.
  • CorepackIntegrityHash — the corepack-form narrowing a packageManager pin needs, plus the one-way SRI bridge: the FromSri codec, the fromSri effect over it, and InvalidSriIntegrityHashError carrying the input that would not convert.
  • ReleaseAgeGate / PartialReleaseAgeGate — pnpm's minimumReleaseAge / minimumReleaseAgeExclude gate as pure vocabulary: combine merges contributions from multiple config sources strictest-age-wins, and filterVersions / isExcluded drop candidate versions younger than the cutoff against a caller-supplied clock, so a resolver never picks a version pnpm would reject. matchesExclude is pnpm's flat-* matcher, deliberately not @effected/glob's dialect. PartialReleaseAgeGate is the permissive inbound form each source contributes.
  • NpmRegistry — registry reads over HttpClient: version, manifest, versions, distTags and publishTimes, each taking a per-call RegistryTarget carrying the registry and an optional credential, with version and manifest reading a GitHub Packages target through the packument; layerTest and the fully-working layerSeeded are the test doubles.
  • VersionManifest — one version's dependencies, peerDependencies, optionalDependencies, peerDependenciesMeta and engines, as NpmRegistry.manifest answers them; the class is also the decoder for a version document.
  • NpmConfig — npmrc and npm_config_* resolution for registry reads: load (with projectDir and workspaceRoot to choose the project npmrc), the pure fromSourcesResult, and registryFor / credentialFor / targetFor, which only ever pair a credential with the host it was configured for; NpmConfigError is its typed failure.
  • PackagePublish — the pack/publish workflow over @effected/commands: setupAuth (taking a PublishCredential), pack, publishTarball (reporting PublishOutcome.auth) and dryRun, with PackedTarball carrying both npm's integrity and a local sha256 digest, and a recording() double that journals setupAuth calls.
  • PackageTarball — fetch, verify and extract a published tarball as a scoped resource, with TarballError routed by reason: notFound | http | integrityMismatch | extractFailed.
  • RegistryCredential — the closed auth union both setupAuth and RegistryTarget take (token for npm's _authToken, basic for its _auth), plus basicCredentialFromPairResult for a caller holding a username and password; WorkloadCredential and workloadCredential for the publish-only workload arm, and PublishCredential for the union setupAuth takes.
  • NpmExecutor — ambient or dlx(spec), the launcher choice a pinned, OIDC-capable npm needs on runners that ship an older one, plus the copying withCacheDir(dir) and withExtraArgs(args).
  • PublishError — the publish-workflow failure, routed by kind: auth | pack | publish | output | digest | executor.
  • RegistryKind / classifyRegistry — classify a registry URL as npm, github-packages, jsr or custom, since provenance and auth differ by kind, with registryShortLabel, registryDisplayName and registryHost rendering one for a log line, prose or a bare host.

The surface grows when a consumer proves it needs more, not before.

License

MIT

版本与发布记录

@effected/npm 共发布 55 个版本,首次发布于 2026/07/17;过去 12 个月发布了 55 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.34.1latest2026/11/13今天
0.34.02026/11/112 天前
0.33.02026/11/102 天前
0.32.12026/11/103 天前
0.32.02026/11/094 天前
0.31.02026/11/094 天前

其他发布通道:next → 0.17.0-next-20260927071821。

关于 @effected/npm 的常见问题

@effected/npm 是什么?

Effect service contracts for resolving pnpm catalog: and workspace: dependency specifiers.

如何安装 @effected/npm?

在项目目录运行 npm install @effected/npm,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:effect(~4.1.0)、@effected/semver(^0.24.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@effected/npm 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@effected/npm 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@effected/npm 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24.11.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@effected/npm 可以商用吗?

@effected/npm 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@effected/npm 的下载量有多少?

根据 npm 官方下载统计,@effected/npm 在 2026/10/13 – 2026/11/11 这 30 天内被下载 10,797 次,日均约 360 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@effected/npm 还在维护吗?

最新版本 0.34.1 发布于 2026/11/13(今天);至今共发布 55 个版本,其中过去 12 个月发布了 55 个;首次发布于 2026/07/17。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。