dnt icon indicating copy to clipboard operation
dnt copied to clipboard

Specifying peer dependency on NPM package does not work

Open joeftiger opened this issue 1 year ago • 3 comments

Hello

I noticed that NPM imports inside deno.json cannot be specified as a peer dependency using @deno/dnt and always end up inside dependencies. When specifying the import using esm.sh/ it works, however.

Minimal example using Blockly as dependency:

deno.json:

{
  "exports": {
    ".": "./mod.ts"
  },
  "imports": {
    "@deno/dnt": "jsr:@deno/dnt@^0.41.3",
    "blockly": "npm:blockly@^11.1.1"
    // "blockly": "https://esm.sh/blockly@^11.1.1"
  }
}

mod.ts:

import "blockly";

build_npm.ts:

import { build, emptyDir } from "@deno/dnt";

await emptyDir("./npm");

await build({
  entryPoints: ["./mod.ts"],
  outDir: "./npm",
  shims: {
    deno: true,
  },
  typeCheck: "both",
  mappings: {
    "npm:blockly@^11.1.1": {
    // "https://esm.sh/blockly@^11.1.1": {
      name: "blockly",
      version: "^11.1.1",
      peerDependency: true,
    },
  },
  package: {
    name: "pure-deno",
    version: "0.0.1",
  },
  importMap: "./deno.json",
});

Replacing the import with the esm.sh/ version correctly transforms blockly as peer dependency while using the npm: version keeps it as direct dependency.

As a workaround one has to keep a separate import map or manually specify dependencies inside package in the build script.

joeftiger avatar Oct 15 '24 14:10 joeftiger

If anyone is running into the same problem right now, my current workaround is the following code snippet to (dirtily) cut out peer dependencies:

const importMap = JSON.parse(Deno.readTextFileSync("./deno.json"));
const blocklyVersion = (<string>importMap["imports"]["blockly"]).split("@")[1];
delete importMap["imports"]["blockly"];
Deno.writeTextFileSync("./deno-npm.json", JSON.stringify(importMap, null, 2));

/// ...
await build({
  // ...
  package: {
    // ...
    peerDependencies: {
      blockly: blocklyVersion,
    },
  },
  importMap: "./deno-npm.json",
});

joeftiger avatar Oct 15 '24 14:10 joeftiger

I'm running into the same problem. For now solving it with a different approach. I overwrite the generated package.json with the unintentional dependencies as dev dependencies.

import type { SpecifierMappings } from "@deno/dnt/transform"

const mappings: SpecifierMappings = {
  "npm:@anthropic-ai/sdk@^0.32.1": {
    name: "@anthropic-ai",
    version: "^0.32.1",
    peerDependency: true,
  },
  "npm:openai@^4.76.0": {
    name: "openai",
    version: "^4.76.0",
    peerDependency: true,
  },
}

await build({
  // ...
  mappings,
})

const packageJsonPath = path.join(outDir, "package.json")
await Deno.readTextFile(packageJsonPath).then(async (v) => {
  const initial = JSON.parse(v)
  const { "@anthropic-ai/sdk": anthropic, openai } = initial.dependencies
  delete initial.dependencies
  initial.peerDependencies = { "@anthropic-ai/sdk": anthropic, openai }
  await Deno.writeTextFile(packageJsonPath, JSON.stringify(initial, null, 2))
})

harrysolovay avatar Dec 16 '24 03:12 harrysolovay

I've also just encountered this issue.

This is how I'm working around it:

  • No dependencies in deno.json.
  • Import dependencies in source files using npm:${package_name} format.
  • Build as normal.
  • Patch the generated package.json.

My build.ts file looks like this:

import { build } from "@deno/dnt";
import { SpecifierMappings } from "@deno/dnt/transform";

const mappings: SpecifierMappings = {
  "npm:${name}": {
    name: "${name}",
    version: "${version}",
    subPath: "${sub_path}",
    peerDependency: ${peer_dependency},
  },
  // more dep mappings...
};

await build({
  // build config...
});

// other post-build steps...

// function to patch the generated package.json
async function updatePackageJson() {
  const packageJsonPath = path.join(DIST_DIR, "package.json");
  const packageJson = JSON.parse(await Deno.readTextFile(packageJsonPath));

  const dependencies = packageJson.dependencies || {};
  const peerDependencies = packageJson.peerDependencies || {};

  for (const [_key, value] of Object.entries(mappings)) {
    if (typeof value === "string") continue;

    const { name, version, peerDependency } = value;

    if (peerDependency) {
      peerDependencies[name] = version;
      delete dependencies[name];
    } else {
      dependencies[name] = version;
    }
  }

  packageJson.dependencies = dependencies;
  packageJson.peerDependencies = peerDependencies;

  await Deno.writeTextFile(
    packageJsonPath,
    JSON.stringify(packageJson, null, 2),
  );
}

await updatePackageJson();

nathanosdev avatar Aug 13 '25 10:08 nathanosdev