Skip to content

Option to disable dependency inlining for RSC directives #157

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

Closed
wobsoriano opened this issue Apr 26, 2025 · 7 comments
Closed

Option to disable dependency inlining for RSC directives #157

wobsoriano opened this issue Apr 26, 2025 · 7 comments
Labels
question Further information is requested

Comments

@wobsoriano
Copy link

Maybe I missed this option in the docs, but I'm looking for a way to disable dependency inlining similar to esbuild's bundle option. This is particularly important when building React libraries that use Server Components, as we need to preserve directives like "use client" in the output.

For example, when building a component like this:

'use client'
import { useState } from 'react'

export function Button() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

Is this something that's already possible? Thanks!

@sxzz
Copy link
Member

sxzz commented Apr 27, 2025

I think it's not relate to dependency inlining, but unbundle mode. You can try the config

defineConfig({
   entry: ["src/**/*.ts"]
})

For preserving directives, maybe you need a plugin https://www.npmjs.com/package/rollup-preserve-directives

@sxzz sxzz modified the milestones: que, removed Apr 27, 2025
@sxzz sxzz added the question Further information is requested label Apr 27, 2025
@wobsoriano
Copy link
Author

wobsoriano commented Apr 27, 2025

The directive is actually preserved, and here's how it looks:

The original client component being built:

'use client'

import { SomeProvider as Provider } from 'some-package';
import type { Props } from 'some-package';

export function ClientProvider({ children, ...rest }: Props) {
  return (
    <Provider {...rest}>
      {children}
    </Provider>
  );
}

The output:

import { ClientProvider } from "../client-provider-BvRQnBtH.js";

export { ClientProvider };
import { SomeProvider as Provider } from "some-package";
import { jsx } from "react/jsx-runtime";

//#region src/client-boundary/client-provider.tsx
"use client";
function ClientProvider({ children,...rest }) {
	return /* @__PURE__ */ jsx(Provider, {
		...rest,
		children
	});
}

//#endregion
export { ClientProvider };
//# sourceMappingURL=client-provider-BvRQnBtH.js.map

Notice where the directive was placed.

I'll try to investigate and play around with it... thanks!

@sxzz
Copy link
Member

sxzz commented Apr 27, 2025

We need a plugin to handle use client directives, but this feature will not be implemented in tsdown.

@wobsoriano
Copy link
Author

wobsoriano commented Apr 27, 2025

Made a custom plugin for this in the meantime

import type { RolldownPlugin } from 'rolldown'

function rollDownPreserveUseClient(): RolldownPlugin {
  return {
    name: 'rolldown-preserve-use-client',
    async writeBundle(options, bundle) {
      for (const [fileName, file] of Object.entries(bundle)) {
        if (file.type !== 'chunk' || !file.code) continue;
        let code = file.code;

        // Find and remove all "use client" or 'use client' directives (with or without semicolon, possibly surrounded by whitespace/newlines)
        const useClientRegex = /^\s*(['"])use client\1\s*;?\s*/gm;
        const matches = [...code.matchAll(useClientRegex)];
        if (matches.length === 0) continue;

        // Remove all occurrences
        let newCode = code.replace(useClientRegex, '');

        // Insert a single 'use client' at the very top, followed by exactly one newline
        const useClientLine = `${matches[0][0].trim()}\n`;
        newCode = useClientLine + newCode.trimStart();

        // Write the modified code back to the file
        const outputPath = path.join(options.dir || path.dirname(options.file), fileName);
        await fs.writeFile(outputPath, newCode, 'utf8');
      }
    }
  };
}

@wobsoriano wobsoriano changed the title Option to disable dependency inlining Option to disable dependency inlining for server component directives Apr 27, 2025
@wobsoriano wobsoriano changed the title Option to disable dependency inlining for server component directives Option to disable dependency inlining for RSC directives Apr 27, 2025
@fuma-nama
Copy link

I think the problem is tsup enables directive preserve by default, this may cause unwanted behaviours when migrating from tsup. It's easy to overlook the directive was gone in the output, would be helpful to notify users on npx tsdown migrate command (or enable this by default to be consistent with tsup)

@uncvrd
Copy link

uncvrd commented May 14, 2025

Also ran into this when migrating from tsup! Here's my simple plugin attempt. I'm no expert at rollup/down etc but I was reading that it may be more performant to perform the modification at the renderChunk phase instead of of the writeBundle phase since you have to read and write back to disk.

Since my package is a UI library I didn't want to important any node files (not a big deal but yea).

Also my linter always updates my "use client" directive to use double quotes so I made a super simple plugin to replace the directive and put it at the top. Also wanted to avoid regex lol. Maybe someone can make an official plugin but here's my shot at it

function MyPlugin(): RolldownPlugin {
    return {
        name: "rolldown-plugin-use-client",
        renderChunk(code) {
            const directive = '"use client";'

            const newCode = code.replace(directive, "")

            // if the newCode doesn't match the existing code, that means we found a directive somewhere in the code and removed it
            if (newCode.length !== code.length) {
                return {
                    code: `${directive}\n\n${newCode}`,
                    map: null,
                }
            } else {
                return null
            }
        },
    }
}

I also don't generate any sourcemaps but I was reading that MagicString would help with that?

Anyways feel free to modify!

@sxzz
Copy link
Member

sxzz commented May 18, 2025

All directives will be preserved now.
https://bundler.sxzz.dev/#eNptjUsKAzEMQ68SvJkWhhwgvUo3JbFLSrBL4tCB4Lt3Qn+boo2EkN4AgjAgc8LNa5ueIfzyCnGPS2/omtYcdTmdeQq3u1R11DlqFnbEh6Mbs7F9hBC0drQVqpSS5ME+ClO++tuX8ad50d7XCenSi35OzewJzsc7sQ==

@sxzz sxzz closed this as completed May 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

4 participants