/** * One physical copy of an identity-sensitive package, enforced at LOAD time. * * ## The gap this closes * * `@nifrajs/web`'s build already pins react/preact/svelte to the app's copy through `onResolve` * (`reactDedupePlugin` and friends), and that covers every bundled phase. It cannot cover an * UNBUNDLED one: Bun's runtime plugin API delivers only the entry point and RELATIVE specifiers to * `onResolve`, so a bare `import "react"` inside a linked package never reaches a resolver hook. The * surfaces that run app sources unbundled - `bun test`, a preloaded script, a route imported natively * - therefore keep loading a second copy no matter what the build does. `onLoad` DOES fire on the * resolved file, which is the one hook that can still intervene, and it is what this module uses. * * It also covers a package class the build plugins never did: `@nifrajs/*` itself. Two copies of * `@nifrajs/core` are two `Server` classes, and `Server` carries private members, so `.merge()` stops * accepting the other's app with a type error that names neither copy. * * ## Why a topology needs it at all * * A package consumed with `link:` (or `file:`, or an `npm link`) resolves ITS OWN imports from where * it physically lives. A shared component library in a sibling repository therefore loads that * repository's react, while the app loads its own - same version, two paths, two module registries. * React reads hooks off a dispatcher the other copy never set, and SSR dies with `Invalid hook call * … resolveDispatcher() is null`, naming neither react nor the package that shadowed it. * * The conventional fixes both cost something real: a cross-repo `workspaces` entry makes the * consumer's install the owner of the other repository's tree and writes into it, and vendoring or * packing copies files that exist precisely so they are not copied. This module is the third option - * leave the topology alone and make the resolution answer correctly - and it is not a workaround: * these packages declare react as a `peerDependency`, which means "the consumer supplies the copy". * That sentence has simply never been enforceable at runtime. Now it is. * * ## What it will not do * * Redirect across a VERSION difference. Two versions is a different defect (someone's range is wrong) * and silently serving 19.2.8 to a package that asked for 19.2.7 turns a loud install problem into a * quiet behavioural one. A version skew is left untouched, so `nifra check` still fails it. * * @example Declare it in package.json, then preload the registrar in bunfig.toml. * ```json * { "nifra": { "singleCopy": ["react", "react-dom", "@nifrajs/*"] } } * ``` * ```toml * preload = ["@nifrajs/core/single-copy/register"] * [test] * preload = ["@nifrajs/core/single-copy/register"] * ``` */ import type { BunPlugin } from "bun"; /** * Packages whose duplication is a defect rather than a waste. Each keeps module-scoped state that * every importer must share - a hook dispatcher, a renderer's options global, a class identity - so a * second physical copy breaks behaviour instead of merely costing bytes. Anything scoped `@nifrajs/` * qualifies for the same reason and is matched by pattern. */ export declare const IDENTITY_SENSITIVE_PACKAGES: readonly string[]; export type SingleCopySkipReason = "version-skew" | "no-counterpart"; /** One foreign copy that will be redirected into the app's copy. */ export interface SingleCopyRedirect { readonly package: string; /** Absolute realpath of the copy that loses - the one a linked package would otherwise load. */ readonly from: string; /** Absolute realpath of the copy that wins - the one resolvable from the app root. */ readonly to: string; readonly version: string; } /** A foreign copy deliberately left alone, and why - never silently dropped. */ export interface SingleCopySkip { readonly package: string; readonly from: string; readonly reason: SingleCopySkipReason; readonly detail: string; } export interface SingleCopyPlan { /** The app root whose installed copy wins. */ readonly root: string; /** Declared package names and patterns, as written. */ readonly declared: readonly string[]; readonly redirects: readonly SingleCopyRedirect[]; readonly skipped: readonly SingleCopySkip[]; } export interface SingleCopyOptions { /** The app root whose copy wins. Defaults to `process.cwd()`. */ readonly cwd?: string; /** Package names or `@scope/*` patterns. Defaults to the `nifra.singleCopy` declaration. */ readonly packages?: readonly string[]; } /** * The declaration, read from `package.json` - deliberately NOT from `nifra.config.ts`. * * `nifra check` holds a pre-load invariant: it never imports the app's config, because importing is * executing. A dedupe claim has to be verifiable by a checker that refuses to run the app, so it lives * in the one file every tool already parses. `true` means the built-in identity-sensitive set. */ export declare function readSingleCopyDeclaration(cwd: string): readonly string[] | undefined; /** * Match a package name against a declaration entry: an exact name, or a `@scope/*` prefix. * * Exported because the enforcement and the CHECK must agree on it exactly. If `nifra check` decided * coverage by its own rule, a package could be reported as deduplicated while the plugin walked past * it - which is worse than no check at all, because it is a green light for a broken graph. */ export declare const matchesSingleCopyDeclaration: (declared: readonly string[], name: string) => boolean; /** The public specifier a `bunfig.toml` preload must name to arm the runtime. */ export declare const SINGLE_COPY_REGISTER_SPECIFIER = "@nifrajs/core/single-copy/register"; /** Which unbundled phases have the resolver preloaded. Bundled phases never need it - nifra's build * injects the plugin itself. */ export interface SingleCopyRegistration { /** `preload` at the top level: covers `bun run` and anything that loads app sources directly. */ readonly run: boolean; /** `[test] preload`: covers `bun test`, the surface that runs app sources unbundled. */ readonly test: boolean; /** The `bunfig.toml` that was read, if one exists. */ readonly config?: string; } /** * Read the runtime proof out of `bunfig.toml`. * * Line-oriented on purpose: this runs inside `nifra check`, which must not execute or import anything * from the project, and the shape being read is a literal array of strings under a known key. A full * TOML parse would buy nothing here and would turn an unrelated syntax error elsewhere in the file * into a failure to answer this question. */ export declare function readSingleCopyRegistration(cwd: string): SingleCopyRegistration; /** * Work out which foreign copies exist and which of them may be redirected. * * Pure discovery: it reads `package.json` files and symlink targets, never application source, and * never mutates anything. Both the plugin and `nifra check`'s verification are built on it, so the * enforcement and the report cannot drift apart. */ export declare function planSingleCopy(options?: SingleCopyOptions): SingleCopyPlan; /** A Bun plugin - `Bun.plugin(...)` for the runtime, or a `plugins:` entry for `Bun.build`. */ export interface SingleCopyPlugin extends BunPlugin { /** What it will do, computed once at construction. Exposed so a caller can report it. */ readonly plan: SingleCopyPlan; } /** * Pin every declared package to the app's copy. * * Two hooks, because the two phases resolve differently. `onResolve` is the direct statement of the * rule and it is what a BUNDLER honours - it sees every bare specifier. The runtime does not deliver * bare specifiers to it at all, so the `onLoad` arm intercepts the foreign file itself and hands back * a re-export of the counterpart in the winning copy. The importer gets the same function objects and * therefore the same module state, which is the entire requirement. */ export declare function singleCopyPlugin(options?: SingleCopyOptions): SingleCopyPlugin; /** Set once the runtime plugin is installed, so a checker can tell "one copy" from "deduplicated". */ export declare const SINGLE_COPY_ACTIVE: unique symbol; /** * Install the plugin into the Bun RUNTIME. Import `@nifrajs/core/single-copy/register` from a * `bunfig.toml` preload rather than calling this from application code: a resolver installed from * inside a module cannot affect the imports that module already resolved. */ export declare function registerSingleCopy(options?: SingleCopyOptions): SingleCopyPlan; //# sourceMappingURL=single-copy.d.ts.map