Skip to content

feat: add stubbing feature#164

Closed
Gugustinette wants to merge 14 commits into
mainfrom
127-built-in-stubbing
Closed

feat: add stubbing feature#164
Gugustinette wants to merge 14 commits into
mainfrom
127-built-in-stubbing

Conversation

@Gugustinette

@Gugustinette Gugustinette commented Apr 27, 2025

Copy link
Copy Markdown
Collaborator

Description

Add a stubbing feature, similar to the unbuild one.

Linked Issues

#127

Additional context

Hi @sxzz ! I tried something for the stubbing feature (based on your stub demo), but this is my first time playing with the tsdown codebase so it is very likely I don't know what I'm doing at all lol

I'd be very happy to get some feedbacks on this !

I guess it lakes a way of doing some kind of JIT transpilation, using jiti as unbuild does :

// dist/index.mjs
import jiti from 'jiti'

export default jiti(null, { interopDefault: true })('/Users/antfu/unbuild-test/src/index')

But first I was mostly asking for a feedback on the overall way of editing the tsdown code 😭

@Gugustinette Gugustinette linked an issue Apr 27, 2025 that may be closed by this pull request
@netlify

netlify Bot commented Apr 27, 2025

Copy link
Copy Markdown

Deploy Preview for tsdown ready!

Name Link
🔨 Latest commit 1bb7d03
🔍 Latest deploy log https://app.netlify.com/sites/tsdown/deploys/68180d488aed43000806352c
😎 Deploy Preview https://deploy-preview-164--tsdown.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@pkg-pr-new

pkg-pr-new Bot commented Apr 27, 2025

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/tsdown@164

commit: 1bb7d03

Comment thread src/features/stubbing.ts Outdated
Comment thread src/features/stubbing.ts Outdated
Comment thread src/features/stubbing.ts Outdated
Comment thread src/features/stubbing.ts Outdated
Comment thread src/index.ts
Comment thread src/options.ts
Comment thread tests/index.test.ts Outdated
Comment thread tests/index.test.ts Outdated
Comment thread src/features/stubbing.ts Outdated
@Gugustinette

Copy link
Copy Markdown
Collaborator Author

I made an example repo to test the feature and compare it to the unbuild stub : https://github.com/Gugustinette/tsdown-stub-test

Of course, for now it does not work, we get this output using unbuild :

import { createJiti } from "../../../node_modules/jiti/lib/jiti.mjs";

const jiti = createJiti(import.meta.url, {
  "interopDefault": true,
  "alias": {
    "@tsdown-stub-test/foo": "/Users/augustinmercier/Desktop/tsdown-stub-test/packages/foo"
  },
  "transformOptions": {
    "babel": {
      "plugins": []
    }
  }
})

/** @type {import("/Users/augustinmercier/Desktop/tsdown-stub-test/packages/foo/src/index")} */
const _module = await jiti.import("/Users/augustinmercier/Desktop/tsdown-stub-test/packages/foo/src/index.ts");

export const foo = _module.foo;

And this output using tsdown :

export * from "/Users/augustinmercier/Desktop/tsdown-stub-test/packages/foo/src/index.ts";
export { default } from "/Users/augustinmercier/Desktop/tsdown-stub-test/packages/foo/src/index.ts";

@Gugustinette

Copy link
Copy Markdown
Collaborator Author

@sxzz Do we copy the unbuild behavior by using jiti ?

Only drawback I see is that it implies adding jiti as a dependency just for the stub feature, and it is 2Mb. But jiti has 0 dependency so that's not too bad I guess ?

I thought about re-creating the jiti behavior ourself, but that might be too much work ?

Curious about how you see things on this side 🤔

@lishaduck

Copy link
Copy Markdown
Contributor

Do we copy the unbuild behavior by using jiti ?

I might consider oxc-node, which is smaller and more consistent (because we use oxc). I think there's an API we could use?

@Gugustinette

Copy link
Copy Markdown
Collaborator Author

So we are talking about oxc-node, published as @oxc-node/cli and @oxc-node/core.

I agree it would be great to use an oxc project for this part too !

I just did some testing, and it lets me run TS files directly as follow :

node ./node_modules/.bin/oxnode ./src/index.ts

But I don't know how far we will get, oxc-node seems a bit experimental at the moment ? Maybe @Brooooooklyn could give us more informations about the project 🤔

@lishaduck

Copy link
Copy Markdown
Contributor

Yeah, I think either oxc-node or unloader would be best, if we can get either of them to expose a jiti-like API. It looks right now, they only support being used as a loader hook.

@Gugustinette

Gugustinette commented Apr 30, 2025

Copy link
Copy Markdown
Collaborator Author

Well it looks like @sxzz used unloader on his demo, so I guess this is what we are going for.

I did not pay attention to this part of his demo, that's my bad 😭

@lishaduck

Copy link
Copy Markdown
Contributor

Well it looks like @sxzz used unloader on his demo, so I guess this is what we are going for.

Yeah, that's what I'd initially meant when I filed the issue, but upon actually looking at it, I don't think unloader offers an importing API (neither does oxnode, but it looks like it'd be pretty easy to work around that 🤷‍♂️).
Then again, it might just be better to make an importing API for unloader, as they're both Kevin's (assuming you approve, @sxzz).

@sxzz

sxzz commented Apr 30, 2025

Copy link
Copy Markdown
Member

Given this decision, I'll take some more time to consider it. Let's postpone the PR for now.

@sxzz
sxzz force-pushed the main branch 2 times, most recently from e5e3d73 to 5e77e49 Compare May 3, 2025 21:08
@Gugustinette

Gugustinette commented May 4, 2025

Copy link
Copy Markdown
Collaborator Author

@sxzz I made some experimental work here : https://github.com/Gugustinette/jitdown

This enables doing such things :

import { jiti } from "../dist/index.mjs";

(async () => {
	const _module = await jiti({
		path: "./playground/module.ts",
	});

	console.log(_module);
	_module.foo("bar");
})();

The trick is just about using the data: protocol to dynamically import the valid JS module created by Rolldown :

import path from "node:path";
import fs from "node:fs";
import type { JitiOptions } from "./options";
import { rolldown } from "rolldown";

// biome-ignore lint/suspicious/noExplicitAny: Dynamically imported modules can't be typed
export const jiti = async (options: JitiOptions): Promise<any> => {
	// Resolve the file path to an absolute path
	const filePath = path.resolve(process.cwd(), options.path);
	// Check if the file exists
	if (!fs.existsSync(filePath)) {
		throw new Error(`File not found: ${filePath}`);
	}

	// Setup bundle
	const bundle = await rolldown({
		// Input options (https://rolldown.rs/reference/config-options#inputoptions)
		input: filePath,
	});

	// Generate bundle in memory
	const rolldownOutput = await bundle.generate({
		// Output options (https://rolldown.rs/reference/config-options#outputoptions)
		format: "esm",
	});

	// Verify that the output is not empty
	if (!rolldownOutput.output[0]) {
		throw new Error("No output chunk found");
	}
	// Get the output chunk
	const outputChunk = rolldownOutput.output[0];

	// Convert code to base64
	const code64 = Buffer.from(outputChunk.code).toString("base64");

	// Create a new module using the base64 encoded code
	const _module = await import(`data:text/javascript;base64,${code64}`);

	// Return the module
	return _module;
};

@sxzz
sxzz force-pushed the 127-built-in-stubbing branch from d35aed4 to 1bb7d03 Compare May 5, 2025 00:58
@sxzz

sxzz commented May 5, 2025

Copy link
Copy Markdown
Member

After careful consideration, I believe that simply providing TypeScript transpilation—similar to what unbuild and jiti offer—is useless for tsdown given the current capabilities of Node.js.

  • We can leverage the exports and publishConfig.exports fields to conditionally select different entry points for development and production modes.
  • In ESM, dynamic named exports are not supported, so any changes to named exports require re-stubbing. Personally, I avoid using stub mode for this reason.
  • If you rely on plugins (such as unplugin-quansync in the unconfig repository), jiti cannot apply these plugins, which may result in runtime errors.
  • Node.js 23 now strips TypeScript types by default (albeit with some limitations), and there are tools like tsx (which is stable) and oxc-node (which is fast) that allow you to run TypeScript files directly, eliminating the need for jiti.

Overall, if we want stub mode to be truly valuable for tsdown, we need:

  • Not only TypeScript transpilation, but also code transformation via Rolldown plugins.
  • Support for dynamic named exports in ESM. If this cannot be addressed, I would prefer to use watch mode instead.

In summary, unloader appears to be a promising option, but it still faces several challenges:

  • Its current adoption and test coverage (reliability) are limited. However, if tsdown adopts it, I believe this will improve over time.
  • For ESM, unloader requires Node.js v18.19 or v20.6 and above; for CJS, it requires Node.js v22.15 or higher. At present, these version requirements are somewhat high.
  • Neither Deno nor Bun have implemented the module.register/registerHooks API, so unfortunately, unloader cannot run on those platforms.

Alternatively, vite-node could be considered, but it is currently too slow, and I have not yet had the opportunity to investigate it further.

/cc @pi0

@Gugustinette

Copy link
Copy Markdown
Collaborator Author

Great investigations !
I agree ; should we add something in the documentation, stating tsdown won't support stub mode (atleast for now) and recommand using watch mode ? Maybe referencing a link to your comment also.

@sxzz

sxzz commented May 5, 2025

Copy link
Copy Markdown
Member

Sure, could you please send a PR for it?

@Gugustinette

Copy link
Copy Markdown
Collaborator Author

Yes !

Where in the doc is that most relevant ? I thought about either :

  • A tip in the watch mode doc
  • A dedicated page in "Advanced", if we want more advanced explanation about what stub is, why It won't be supported,...
  • Adding Q&A section ? I did this in Nuxt Leaflet and I feel it can be useful for common questions, instead of searching through issues (as the Q&A is searchable through the search bar) : https://leaflet.nuxtjs.org/about/q&a.html

@sxzz

sxzz commented May 5, 2025

Copy link
Copy Markdown
Member

Yeah, we should add a FAQ (Frequently Asked Questions) page, which is a good idea.

@Gugustinette Gugustinette mentioned this pull request May 5, 2025
@sxzz sxzz closed this in #201 May 5, 2025
@Gugustinette
Gugustinette deleted the 127-built-in-stubbing branch May 5, 2025 22:51
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

Successfully merging this pull request may close these issues.

Built-in stubbing

3 participants