metafloor / bwip-js

Barcode Writer in Pure JavaScript
Other
2.12k stars 304 forks source link

Lack of `default` condition in the exports map #306

Closed james-yeoman closed 12 months ago

james-yeoman commented 1 year ago

According to the NodeJS docs (See the paragraph before the linked anchor), the default condition should always be defined when you have an exports map. This is because it allows developer tools to avoid imitating a platform like browser in order to resolve files successfully.

bwip-js is the current blocker in me adopting the new bundler module resolution for Typescript. This is because tsc is unable to resolve bwip-js, as it doesn't have a default condition.

Since I have code that gets bundled for both the browser and node environments (an internal library that gets imported for those two environments), I can't just specify "customConditions": ["browser"] in my library's tsconfig.json.

I'm able to get the resolution working fine with my bundlers, but tsc is the only remaining problem.

Is there a generic version of bwip-js that can be used in esm and cjs environments? If so, a default condition would be greatly appreciated.

Even if there isn't, I'd like to figure out a solution to this

metafloor commented 1 year ago

There is no default, as each module is targeted to the specific features of the platform. And it makes no sense to include a default since it will likely be wrong half the time. I agree with you that a solution to this is needed. My question is: what is tsc looking for in the exports map before it falls back to the default? It appears that it is not looking for the node and browser targets. Maybe nodenext?

james-yeoman commented 1 year ago

Using --traceResolution, the Bundler module resolution only uses import and types conditions

james-yeoman commented 1 year ago

It also appears that using customConditions in the app tsconfig and rendering lambda's tsconfig doesn't cascade to the renderer library. I'll go and make a bug report on the typescript repo

james-yeoman commented 1 year ago

Hmm... while creating a reproduction repo for the typescript repo bug report, I realised something else: the IDE can't resolve bwip-js for the exact same reason. I'm going to consult with the rest of my team, but I'm leaving this issue open in the meantime

james-yeoman commented 1 year ago

@metafloor I've made a repo to reproduce the exact problem this issue was created for. I've also detailed a bit more thoroughly about the use-case in the readme.

Also, I was wondering: what if you had the default condition resolve to a version that only had functions that have a DrawingContext parameter? That looks like the shared interface between the different platforms to me, but I may well be completely wrong there.

If people wanted to use "moduleResolution": "Bundler" and one of the helper functions, they'd need to specify the appropriate custom conditions in their tsconfig, sure. But at that point, they're much more likely to only be targeting one platform, since they'd be getting platform-specific types from those functions.

metafloor commented 1 year ago

I am very leery of creating a default that does not match the readme. It will cause no end of grief when a bundler chooses incorrectly. I would rather the bundler fail to resolve (as with this current issue), so we can better understand where improvements can be made in package.json (see #298 for example) or in a bundler's imports resolution.

nodejs did a good job of documenting how export maps are supposed to be used. Now we just need the bundler implementations to work out the kinks using real world projects like bwip-js that provide different modules for different platforms.

I am still confused why tsc is not looking at either the browser or node targets. Don't you need to tell tsc the underlying platform it should be targeting?

james-yeoman commented 1 year ago

The bundler module resolution mode makes no assumptions about the platform you're targeting. Since I use vite for building the frontend, the browser condition gets resolved. On the backend, I use esbuild, and it's targetting node.

The problem is with typechecking the library, since it doesn't have a specific platform that it targets.

I may very well be in unusual territories with my setup, but nonetheless, it's a painpoint for me that I'd like to see some way of resolving. I'm not in unusual territories, because npx create-vite, yarn create vite, and the other similar project scaffold executables default to "moduleResolution": "Bundler".

It's something that'll become more commonplace as time goes on.

At the moment, I'm looking at using the following declaration file in order to use the platform-agnostic declarations (trimmed down to only the signatures that the renderer lib needs)

Details ```ts // Taken from bwip-js/dist/bwip-js.d.ts and stripped down to only what we need // // Necessary until https://github.com/metafloor/bwip-js/issues/306 is resolved // // This enables Typescript and VS Code to typecheck without needing any custom // conditions for exports map resolution. // // Our buildtools can resolve the exports map correctly, as they have the neccessary // context at build time to decide which version to use (vite uses the "browser" // condition, and @lll/api uses esbuild to bundle for node, so would use the "node" // condition) // I know... reaching into node_modules is a sin... it's the only way to do this // since the `declare module` line after, masks the `bwip-js` import specifier // globally, so I can't do `import type {...} from "bwip-js/dist/bwip-js" import type { RenderOptions, DrawingContext } from "../node_modules/bwip-js/dist/bwip-js"; declare module "bwip-js" { export namespace BwipJs { export type { RenderOptions, DrawingContext }; // Can't select which overloads to import, so have to copy signatures instead export function code128( opts: RenderOptions, drawing: DrawingContext> ): Promise; export function code128( opts: RenderOptions, drawing: DrawingContext ): T; export function ean13( opts: RenderOptions, drawing: DrawingContext> ): Promise; export function ean13( opts: RenderOptions, drawing: DrawingContext ): T; export function ean8( opts: RenderOptions, drawing: DrawingContext> ): Promise; export function ean8( opts: RenderOptions, drawing: DrawingContext ): T; export function gs1_128( opts: RenderOptions, drawing: DrawingContext> ): Promise; export function gs1_128( opts: RenderOptions, drawing: DrawingContext ): T; export function itf14( opts: RenderOptions, drawing: DrawingContext> ): Promise; export function itf14( opts: RenderOptions, drawing: DrawingContext ): T; export function qrcode( opts: RenderOptions, drawing: DrawingContext> ): Promise; export function qrcode( opts: RenderOptions, drawing: DrawingContext ): T; export function upca( opts: RenderOptions, drawing: DrawingContext> ): Promise; export function upca( opts: RenderOptions, drawing: DrawingContext ): T; } export = BwipJs; } ```

and so far, the results seem promising. Typechecking is successful in the frontend, backend, and the renderer library.

Now to test that it builds correctly

james-yeoman commented 1 year ago

It indeed builds correctly.

Since customConditions will allow people to specify their build platform, we come back to the scenario of "I'm using this in a kind-of non-standard way, how can we solve this so people don't have problems in the future".

What if you used subpath mapping to have bwip-js/web, bwip-js/node, etc, and then have a bwip-js/context-only that resembles my declaration file in the above comment?

james-yeoman commented 1 year ago

In fact, how about having the core resemble my declaration file (only expose functions with drawing contexts as params), and then have separate DrawingContexts for web and node?

That would likely allow you to remove the platform-specific dependencies for the core api, and still support multiple platforms.

If anything, it'd make it easier to support new platforms in the future, as you'd only need to implement a new DrawingContext

metafloor commented 1 year ago

I am not thrilled with this idea as it fails to support the generic methods like toBuffer() and toCanvas() which are heavily used by most of the users. My current thinking is to provide import paths that target a specific platform. For example import { ... } from "bwip-js/browser" or import bwipjs from "bwip-js/node". Inside those export map entries would be the platform specific type definitions. And the current export map would be re-homed to the . path, which should maintain backward compatibility.

The problem is I have no way of knowing if it will work. I can test against the usual bundlers, but I don't use typescript or vs code and am not willing to go down that rabbit hole.

And the solution must maintain backward compatibility with non-typescript users.

james-yeoman commented 1 year ago

That sounds like a decent compromise, but the point still stands that I have a case where I don't need the toBuffer or toCanvas methods, and would prefer a platform-agnostic solution.

Would it be possible to do as you've outlined, while also providing a bwip-js/context-only or bwip-js/drawing-context path that excludes all platform-specific methods? This wouldn't necessarily need to be backwards-compatible, so could exist as just a path mapping entry

metafloor commented 1 year ago

Why would you need a module that does not include the generic methods? That makes no sense. This solution is already platform-specific and your bundler-of-choice will tree-shake the module to only include what is used by your code.

james-yeoman commented 1 year ago

Because my team has a platform-agnostic rendering system that we hook bwip-js up to, using DrawingContext, in order to render barcodes on all the platforms we have.

We don't make use of toCanvas and toBuffer. We exclusively use the DrawingContext signatures (i.e. export function ean8(opts: RenderOptions, drawing: DrawingContext<T>): T;) and implement DrawingContext for each of our platforms.

metafloor commented 1 year ago

Alright, so I will add a generic target as well, but it won't happen right away. I am on holiday for the next two weeks and will expect the usual chaos the week after I return....

james-yeoman commented 1 year ago

Thank you. We appreciate the effort you put into this library, and want to see the best for it.

Enjoy your time off!

james-yeoman commented 12 months ago

@metafloor I see you've not been able to hit the self-imposed deadline you mentioned in 310. Is there anything I can do to help?

metafloor commented 12 months ago

v4.1.2 provides enumerated platform targets in package.json. The readme has not been updated because I want to ensure this is working without breaking backward compatibility. Hopefully not too much screaming will result....

The previous exports map has been moved to the . target and the following explicit targets have been added:

./browser
./node
./electron
./react-native
./generic

To import just the cross-platform code, you would use:

import { toSVG, qrcode } from 'bwip-js/generic';

Please let me know if that works for your use case.

james-yeoman commented 12 months ago

Yes, this works perfectly for me! Thank you!