Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DNT should support "workspace" (monorepo) libraries, in the same way Deno does #430

Open
masonmark opened this issue Sep 28, 2024 · 2 comments

Comments

@masonmark
Copy link

It would be useful if DNT could be made to support the "workspace" feature of Deno, so that it could package local monorepo library code in the same way Deno does. That is, when encountering an import statement like this:

import { Hoge, SomethingElse } from 'jsr:@axhxrx/ts';

...and if the deno.jsonc specifies that this library is part of the current monorepo/workspace, via a workspace configuration like this:

{
  "workspace": [
    "libs/jsr/@axhxrx/json",
    "libs/jsr/@axhxrx/ts"
  ]
}

...then the import should automagically resolve to the local monorepo version of the library, and not actually download it from JSR. (In other words, it should resolve the import in the same way that deno run and deno test do.)

Our use case here is a Deno monorep, using the "workspace" feature, which contains multiple libraries, that get published to JSR. That works very well for letting other code consume them, but when we are working in the monorepo, we want to use the local monorepo versions of the library.

Where DNT comes into this picture is, we are trying to use DNT to produce an alternate build of our libraries, as local NPM packages (that build into ./dist). This is a fallback mechanism for legacy/enterprise code, which cannot use the library code directly, because it doesn't work with imports that have the .ts filename extension, etc. The NPM packages are easily consumable by the legacy code; modern code can just use the libraries as-is in the monorepo, without a build step, using tools like Deno (or Bun, etc.).

Our problem is that DNT does not seem to understand the "workspace" feature of Deno. So when we use DNT to build and NPM package from library A, which depends on library B, DNT tries to fetch library B from JSR. That might sometimes "work" (albeit very slowly), but it will fail if JSR doesn't yet have the version of library B.

We can sort of fix this problem by manually adding an "importMap" field to the monorepo's deno.jsonc file:

{
  "workspace": [
    "libs/jsr/@axhxrx/json",
    "libs/jsr/@axhxrx/ts"
  ],
  "imports": {
    "@deno/dnt": "jsr:@deno/dnt@^0.41.3",
    "jsr:@axhxrx/ts": "./libs/jsr/@axhxrx/ts/mod.ts"
  }
}

But, this is cumbersome (especially when we start importing specific versions of things), and also it seems to be fighting against what the "workspace" feature is doing for us. It does work, but it's hard to see at a glance which of those two things is going to have precedence when resolving "jsr:@axhxrx/ts".

It feels like DNT should (perhaps optionally?) support Deno "workspace" out of the box. If that's something that would be of interest, I might be able to try to contribute a PR (but would be delighted if you just fixed it, lol).

I have produced a test repo that reproduces/demonstrates the issue: https://github.com/axhxrx/jsr-deno-nx-monorepo-test

@Gaubee
Copy link
Contributor

Gaubee commented Oct 12, 2024

You can try my implementation on a simple project: https://jsr.io/@gaubee/denokit/doc/dnt_monorepo

In my own project https://github.com/Gaubee/std, there is a need to compile deno-workspace into an npm-monorepo. Therefore, I implemented this requirement simply in my project and applied it here. For reference, see the code:

https://github.com/Gaubee/std/blob/main/dnt.ts

@MasterKale
Copy link

I'm in a similar situation, which is blocking testing of the otherwise successfully dnt-generated output.

./deno.jsonc

{
  // ...
  "imports": {
    "@std/assert": "jsr:@std/assert@^1.0.7",
    // ...
  },
  "workspace": [
    "./packages/server",
    "./packages/types"
  ]
}

./packages/server/build_npm.ts

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

const outDir = './npm';

const denoJSON: { version: string } = JSON.parse(
  Deno.readTextFileSync('./deno.jsonc'),
);
const typesDenoJSON: { version: string } = JSON.parse(
  Deno.readTextFileSync('../types/deno.jsonc'),
);

await emptyDir(outDir);

await build({
  entryPoints: [
    { name: '.', path: './src/index.ts' },
    { name: './helpers', path: './src/helpers/index.ts' },
  ],
  outDir,
  importMap: './deno.jsonc',
  shims: {
    deno: {
      test: 'dev',
    },
  },
  // TODO: Re-enable if https://github.com/denoland/dnt/issues/331 can get resolved
  typeCheck: false,
  // package.json values
  package: {
    // ...
  },
  // Map from Deno package to NPM package for Node build
  mappings: {},
  // TypeScript tsconfig.json config
  compilerOptions: {
    lib: ['ES2021'],
  },
});

Deno.copyFileSync('LICENSE.md', `${outDir}/LICENSE.md`);
Deno.copyFileSync('README.md', `${outDir}/README.md`);

There's no mention of "@std/assert" in ./packages/server/deno.json because it's not needed with workspaces, right? At least, deno test commands run just fine in the various packages as normal Deno tests.

Then I run the dnt build task...

packages/server/ $> deno task build
Task build deno run -A build_npm.ts
[dnt] Transforming...
[dnt] Running npm install...

added 22 packages, and audited 23 packages in 905ms

found 0 vulnerabilities
[dnt] Building project...
[dnt] Emitting ESM package...
[dnt] Emitting script package...
[dnt] Running tests...

> @simplewebauthn/server@11.0.0-alpha2 test
> node test_runner.js

Running tests in ./script/authentication/generateAuthenticationOptions.test.js...

Error: Cannot find module '@std/assert'
Require stack:
- /Users/matt/Developer/simplewebauthn/packages/server/npm/script/authentication/generateAuthenticationOptions.test.js
- /Users/matt/Developer/simplewebauthn/packages/server/npm/test_runner.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1225:15)
    at Module._load (node:internal/modules/cjs/loader:1051:27)
    at Module.require (node:internal/modules/cjs/loader:1311:19)
    at require (node:internal/modules/helpers:179:18)
    at Object.<anonymous> (/Users/matt/Developer/simplewebauthn/packages/server/npm/script/authentication/generateAuthenticationOptions.test.js:27:18)
    at Module._compile (node:internal/modules/cjs/loader:1469:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1548:10)
    at Module.load (node:internal/modules/cjs/loader:1288:32)
    at Module._load (node:internal/modules/cjs/loader:1104:12)
    at Module.require (node:internal/modules/cjs/loader:1311:19) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    '/Users/matt/Developer/simplewebauthn/packages/server/npm/script/authentication/generateAuthenticationOptions.test.js',
    '/Users/matt/Developer/simplewebauthn/packages/server/npm/test_runner.js'
  ]
}
error: Uncaught (in promise) Error: npm run test failed with exit code 1
      throw new Error(
            ^
    at runCommand (https://jsr.io/@deno/dnt/0.41.3/lib/utils.ts:56:13)
    at eventLoopTick (ext:core/01_core.js:175:7)
    at async build (https://jsr.io/@deno/dnt/0.41.3/mod.ts:419:5)
    at async file:///Users/matt/Developer/simplewebauthn/packages/server/build_npm.ts:14:1

(dnt output) ./packages/server/npm/script/authentication/generateAuthenticationOptions.test.js

// ...
const assert_1 = require("@std/assert");
// ...

If I add test: false to my call to dnt's build() then the build task exits gracefully; it's just the attempt to test the code that chokes on that reference to @std/assert, which is mapped in the monorepo's root deno.json

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants